diff options
Diffstat (limited to 'apps/dav')
62 files changed, 1091 insertions, 46 deletions
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index a100dac1d85..a9bf60698fd 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -143,6 +143,7 @@ return array( 'OCA\\DAV\\Connector\\LegacyDAVACL' => $baseDir . '/../lib/Connector/LegacyDAVACL.php', 'OCA\\DAV\\Connector\\PublicAuth' => $baseDir . '/../lib/Connector/PublicAuth.php', 'OCA\\DAV\\Connector\\Sabre\\AnonymousOptionsPlugin' => $baseDir . '/../lib/Connector/Sabre/AnonymousOptionsPlugin.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', @@ -310,8 +311,10 @@ return array( '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\\UploadFile' => $baseDir . '/../lib/Upload/UploadFile.php', 'OCA\\DAV\\Upload\\UploadFolder' => $baseDir . '/../lib/Upload/UploadFolder.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 4187bb6c6f3..48104281cd4 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -158,6 +158,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Connector\\LegacyDAVACL' => __DIR__ . '/..' . '/../lib/Connector/LegacyDAVACL.php', 'OCA\\DAV\\Connector\\PublicAuth' => __DIR__ . '/..' . '/../lib/Connector/PublicAuth.php', 'OCA\\DAV\\Connector\\Sabre\\AnonymousOptionsPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AnonymousOptionsPlugin.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', @@ -325,8 +326,10 @@ class ComposerStaticInitDAV '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\\UploadFile' => __DIR__ . '/..' . '/../lib/Upload/UploadFile.php', 'OCA\\DAV\\Upload\\UploadFolder' => __DIR__ . '/..' . '/../lib/Upload/UploadFolder.php', diff --git a/apps/dav/l10n/bg.js b/apps/dav/l10n/bg.js index e43c36c2e1a..a5931ec3bbb 100644 --- a/apps/dav/l10n/bg.js +++ b/apps/dav/l10n/bg.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "„%1$s“ е отказано", "Re: %1$s" : "Re: %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:" : "Organizer/организатор/:", diff --git a/apps/dav/l10n/bg.json b/apps/dav/l10n/bg.json index 284f165abd9..c8d8d6e3ae7 100644 --- a/apps/dav/l10n/bg.json +++ b/apps/dav/l10n/bg.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "„%1$s“ е отказано", "Re: %1$s" : "Re: %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:" : "Organizer/организатор/:", diff --git a/apps/dav/l10n/cs.js b/apps/dav/l10n/cs.js index fa814d575e0..ce1e7d55b54 100644 --- a/apps/dav/l10n/cs.js +++ b/apps/dav/l10n/cs.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "„%1$s“ bylo zrušeno", "Re: %1$s" : "Odp.: %1$s", "%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", "%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:", @@ -129,7 +131,7 @@ OC.L10N.register( "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.", + "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", diff --git a/apps/dav/l10n/cs.json b/apps/dav/l10n/cs.json index 9234e5b9d99..4ecd597e1bd 100644 --- a/apps/dav/l10n/cs.json +++ b/apps/dav/l10n/cs.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "„%1$s“ bylo zrušeno", "Re: %1$s" : "Odp.: %1$s", "%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", "%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:", @@ -127,7 +129,7 @@ "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.", + "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", diff --git a/apps/dav/l10n/de.js b/apps/dav/l10n/de.js index 0765e4406e5..adff7fabf43 100644 --- a/apps/dav/l10n/de.js +++ b/apps/dav/l10n/de.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "\"%1$s\" wurde abgebrochen.", "Re: %1$s" : "Re: %1$s", "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s möchte dich zu \"%2$s\" einladen.", "Organizer:" : "Organisator:", diff --git a/apps/dav/l10n/de.json b/apps/dav/l10n/de.json index 368950ad5d1..3aecada1d1f 100644 --- a/apps/dav/l10n/de.json +++ b/apps/dav/l10n/de.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "\"%1$s\" wurde abgebrochen.", "Re: %1$s" : "Re: %1$s", "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s möchte dich zu \"%2$s\" einladen.", "Organizer:" : "Organisator:", diff --git a/apps/dav/l10n/de_DE.js b/apps/dav/l10n/de_DE.js index 1868f3a9190..ab170d444a3 100644 --- a/apps/dav/l10n/de_DE.js +++ b/apps/dav/l10n/de_DE.js @@ -72,9 +72,11 @@ OC.L10N.register( "Where: %s" : "Ort: %s", "%1$s via %2$s" : "%1$s über %2$s", "Cancelled: %1$s" : "Abgesagt: %1$s", - "\"%1$s\" has been canceled" : "\"%1$s\" wurde abgebrochen", + "\"%1$s\" has been canceled" : "\"%1$s“ wurde abgesagt.", "Re: %1$s" : "Re: %1$s", "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s möchte Sie zu \"%2$s\" einladen", "Organizer:" : "Organisator:", diff --git a/apps/dav/l10n/de_DE.json b/apps/dav/l10n/de_DE.json index 09d29f0ce62..ab756378088 100644 --- a/apps/dav/l10n/de_DE.json +++ b/apps/dav/l10n/de_DE.json @@ -70,9 +70,11 @@ "Where: %s" : "Ort: %s", "%1$s via %2$s" : "%1$s über %2$s", "Cancelled: %1$s" : "Abgesagt: %1$s", - "\"%1$s\" has been canceled" : "\"%1$s\" wurde abgebrochen", + "\"%1$s\" has been canceled" : "\"%1$s“ wurde abgesagt.", "Re: %1$s" : "Re: %1$s", "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s möchte Sie zu \"%2$s\" einladen", "Organizer:" : "Organisator:", diff --git a/apps/dav/l10n/en_GB.js b/apps/dav/l10n/en_GB.js index ac17c21b6d4..4c8ab334d66 100644 --- a/apps/dav/l10n/en_GB.js +++ b/apps/dav/l10n/en_GB.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "\"%1$s\" has been cancelled", "Re: %1$s" : "Re: %1$s", "%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:", diff --git a/apps/dav/l10n/en_GB.json b/apps/dav/l10n/en_GB.json index ee386676c4a..1a9010fe006 100644 --- a/apps/dav/l10n/en_GB.json +++ b/apps/dav/l10n/en_GB.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "\"%1$s\" has been cancelled", "Re: %1$s" : "Re: %1$s", "%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:", diff --git a/apps/dav/l10n/es.js b/apps/dav/l10n/es.js index 507b822cb20..2ae42f1c897 100644 --- a/apps/dav/l10n/es.js +++ b/apps/dav/l10n/es.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "\"%1$s\" ha sido cancelada", "Re: %1$s" : "Re: %1$s", "%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 invitarle a \"%2$s\"", "Organizer:" : "Organizador:", diff --git a/apps/dav/l10n/es.json b/apps/dav/l10n/es.json index d0b55436281..203c52acb65 100644 --- a/apps/dav/l10n/es.json +++ b/apps/dav/l10n/es.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "\"%1$s\" ha sido cancelada", "Re: %1$s" : "Re: %1$s", "%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 invitarle a \"%2$s\"", "Organizer:" : "Organizador:", diff --git a/apps/dav/l10n/eu.js b/apps/dav/l10n/eu.js index fbc0f4f196f..6e02ca53f49 100644 --- a/apps/dav/l10n/eu.js +++ b/apps/dav/l10n/eu.js @@ -74,6 +74,9 @@ OC.L10N.register( "Cancelled: %1$s" : "Utzita: %1$s", "\"%1$s\" has been canceled" : "\"%1$s\" bertan behera utzi da", "Re: %1$s" : "Er: %1$s", + "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s-k \"%2$s\"-ra gonbidatu nahi zaitu", "Organizer:" : "Antolatzailea:", diff --git a/apps/dav/l10n/eu.json b/apps/dav/l10n/eu.json index 88f2cfd40d2..14f1f3de4a5 100644 --- a/apps/dav/l10n/eu.json +++ b/apps/dav/l10n/eu.json @@ -72,6 +72,9 @@ "Cancelled: %1$s" : "Utzita: %1$s", "\"%1$s\" has been canceled" : "\"%1$s\" bertan behera utzi da", "Re: %1$s" : "Er: %1$s", + "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s-k \"%2$s\"-ra gonbidatu nahi zaitu", "Organizer:" : "Antolatzailea:", diff --git a/apps/dav/l10n/fr.js b/apps/dav/l10n/fr.js index 7008bb8dc9f..95dd7ff3414 100644 --- a/apps/dav/l10n/fr.js +++ b/apps/dav/l10n/fr.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "\"%1$s\" a été annulé(e)", "Re: %1$s" : "Re : %1$s", "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s souhaite vous inviter à \"%2$s\"", "Organizer:" : "Organisateur :", diff --git a/apps/dav/l10n/fr.json b/apps/dav/l10n/fr.json index 20d11c43640..60901d2d3ff 100644 --- a/apps/dav/l10n/fr.json +++ b/apps/dav/l10n/fr.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "\"%1$s\" a été annulé(e)", "Re: %1$s" : "Re : %1$s", "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s souhaite vous inviter à \"%2$s\"", "Organizer:" : "Organisateur :", diff --git a/apps/dav/l10n/gl.js b/apps/dav/l10n/gl.js index e308f11a3bf..247c5688f61 100644 --- a/apps/dav/l10n/gl.js +++ b/apps/dav/l10n/gl.js @@ -173,7 +173,7 @@ OC.L10N.register( "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.", "Send reminder notifications to calendar sharees as well" : "Envía notificacións de recordatorio tamén aos que comparten calendario", "Reminders are always sent to organizers and attendees." : "Os recordatorios sempre se envían aos organizadores e aos asistentes.", - "Enable notifications for events via push" : "Activar o envío de notificacións do servidor para eventos", + "Enable notifications for events via push" : "Activar o envío de notificacións do automáticas 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}.", "There was an error updating your attendance status." : "Produciuse un erro ao actualizar o seu estado de asistencia.", diff --git a/apps/dav/l10n/gl.json b/apps/dav/l10n/gl.json index fd996dfa844..a16ef36d78f 100644 --- a/apps/dav/l10n/gl.json +++ b/apps/dav/l10n/gl.json @@ -171,7 +171,7 @@ "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.", "Send reminder notifications to calendar sharees as well" : "Envía notificacións de recordatorio tamén aos que comparten calendario", "Reminders are always sent to organizers and attendees." : "Os recordatorios sempre se envían aos organizadores e aos asistentes.", - "Enable notifications for events via push" : "Activar o envío de notificacións do servidor para eventos", + "Enable notifications for events via push" : "Activar o envío de notificacións do automáticas 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}.", "There was an error updating your attendance status." : "Produciuse un erro ao actualizar o seu estado de asistencia.", diff --git a/apps/dav/l10n/hu.js b/apps/dav/l10n/hu.js index d47163cf8f5..6fd13b3deef 100644 --- a/apps/dav/l10n/hu.js +++ b/apps/dav/l10n/hu.js @@ -74,6 +74,9 @@ OC.L10N.register( "Cancelled: %1$s" : "Lemondva: %1$s", "\"%1$s\" has been canceled" : "A következőt le lett mondva: „%1$s”", "Re: %1$s" : "Vá: %1$s", + "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s meg szeretné hívni a következőre: „%2$s”", "Organizer:" : "Szervező:", diff --git a/apps/dav/l10n/hu.json b/apps/dav/l10n/hu.json index 60ef5e10029..b2355dc43ce 100644 --- a/apps/dav/l10n/hu.json +++ b/apps/dav/l10n/hu.json @@ -72,6 +72,9 @@ "Cancelled: %1$s" : "Lemondva: %1$s", "\"%1$s\" has been canceled" : "A következőt le lett mondva: „%1$s”", "Re: %1$s" : "Vá: %1$s", + "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s meg szeretné hívni a következőre: „%2$s”", "Organizer:" : "Szervező:", diff --git a/apps/dav/l10n/pl.js b/apps/dav/l10n/pl.js index 32c4d158ce0..db40de74803 100644 --- a/apps/dav/l10n/pl.js +++ b/apps/dav/l10n/pl.js @@ -72,8 +72,13 @@ OC.L10N.register( "Where: %s" : "Gdzie: %s", "%1$s via %2$s" : "%1$s przez %2$s", "Cancelled: %1$s" : "Anulowane: %1$s", + "\"%1$s\" has been canceled" : "\"%1$s\" zostało anulowane", "Re: %1$s" : "Odp: %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", + "%1$s would like to invite you to \"%2$s\"" : "%1$s chce cię zaprosić na \"%2$s\"", "Organizer:" : "Organizator:", "Attendees:" : "Uczestnicy:", "Title:" : "Tytuł:", diff --git a/apps/dav/l10n/pl.json b/apps/dav/l10n/pl.json index 2784dc03ed4..90c75076c0a 100644 --- a/apps/dav/l10n/pl.json +++ b/apps/dav/l10n/pl.json @@ -70,8 +70,13 @@ "Where: %s" : "Gdzie: %s", "%1$s via %2$s" : "%1$s przez %2$s", "Cancelled: %1$s" : "Anulowane: %1$s", + "\"%1$s\" has been canceled" : "\"%1$s\" zostało anulowane", "Re: %1$s" : "Odp: %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", + "%1$s would like to invite you to \"%2$s\"" : "%1$s chce cię zaprosić na \"%2$s\"", "Organizer:" : "Organizator:", "Attendees:" : "Uczestnicy:", "Title:" : "Tytuł:", diff --git a/apps/dav/l10n/pt_BR.js b/apps/dav/l10n/pt_BR.js index 802d9531aba..9cd73a722bc 100644 --- a/apps/dav/l10n/pt_BR.js +++ b/apps/dav/l10n/pt_BR.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "\"%1$s\" foi cancelado", "Re: %1$s" : "Remetente: %1$s", "%1$s has responded to your invitation" : "%1$s respondeu ao seu convite", + "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" : "Convite: %1$s", "%1$s would like to invite you to \"%2$s\"" : "%1$s gostaria de convidá-lo para \"%2$s\"", "Organizer:" : "Organizador:", diff --git a/apps/dav/l10n/pt_BR.json b/apps/dav/l10n/pt_BR.json index 4224071d29d..6a9765cb206 100644 --- a/apps/dav/l10n/pt_BR.json +++ b/apps/dav/l10n/pt_BR.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "\"%1$s\" foi cancelado", "Re: %1$s" : "Remetente: %1$s", "%1$s has responded to your invitation" : "%1$s respondeu ao seu convite", + "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" : "Convite: %1$s", "%1$s would like to invite you to \"%2$s\"" : "%1$s gostaria de convidá-lo para \"%2$s\"", "Organizer:" : "Organizador:", diff --git a/apps/dav/l10n/ru.js b/apps/dav/l10n/ru.js index b6eab41df26..95e2fa9e5e6 100644 --- a/apps/dav/l10n/ru.js +++ b/apps/dav/l10n/ru.js @@ -72,8 +72,13 @@ OC.L10N.register( "Where: %s" : "Где: %s", "%1$s via %2$s" : "%1$s через %2$s", "Cancelled: %1$s" : "Событие отменено: %1$s", + "\"%1$s\" has been canceled" : "Событие «%1$s» отменено", "Re: %1$s" : "Re: %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:" : "Название:", @@ -110,11 +115,20 @@ OC.L10N.register( "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>", "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 байт","%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" : "Не удалось открыть файл", "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." : "Сервер находится в режиме обслуживания.", @@ -128,6 +142,7 @@ OC.L10N.register( "Completed on %s" : "Завершено %s", "Due on %s by %s" : "До %s %s", "Due on %s" : "До %s", + "Migrated calendar (%1$s)" : "Перенос календаря (%1$s)", "Calendars including events, details and attendees" : "Календари, в том числе события, подробные сведения и участники", "Contacts and groups" : "Контакты и группы", "WebDAV" : "WebDAV", @@ -146,8 +161,11 @@ OC.L10N.register( "Friday" : "Пятница", "Saturday" : "Суббота", "Sunday" : "Воскресенье", + "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматически изменять статус на «Не беспокоить» вне интервала доступности для отключения уведомлений.", "Save" : "Сохранить", "Failed to load availability" : "Не удалось получить сведения о доступности", + "Saved availability" : "Сведения о доступности сохранены", + "Failed to save availability" : "Не удалось сохранить сведения о доступности", "Calendar server" : "Сервер календаря", "Send invitations to attendees" : "Отправить приглашения", "Automatically generate a birthday calendar" : "Создавать календарь дней рождения автоматически", @@ -155,6 +173,8 @@ 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}.", diff --git a/apps/dav/l10n/ru.json b/apps/dav/l10n/ru.json index 9c7625efd74..13ae27e6751 100644 --- a/apps/dav/l10n/ru.json +++ b/apps/dav/l10n/ru.json @@ -70,8 +70,13 @@ "Where: %s" : "Где: %s", "%1$s via %2$s" : "%1$s через %2$s", "Cancelled: %1$s" : "Событие отменено: %1$s", + "\"%1$s\" has been canceled" : "Событие «%1$s» отменено", "Re: %1$s" : "Re: %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:" : "Название:", @@ -108,11 +113,20 @@ "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>", "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 байт","%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" : "Не удалось открыть файл", "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." : "Сервер находится в режиме обслуживания.", @@ -126,6 +140,7 @@ "Completed on %s" : "Завершено %s", "Due on %s by %s" : "До %s %s", "Due on %s" : "До %s", + "Migrated calendar (%1$s)" : "Перенос календаря (%1$s)", "Calendars including events, details and attendees" : "Календари, в том числе события, подробные сведения и участники", "Contacts and groups" : "Контакты и группы", "WebDAV" : "WebDAV", @@ -144,8 +159,11 @@ "Friday" : "Пятница", "Saturday" : "Суббота", "Sunday" : "Воскресенье", + "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматически изменять статус на «Не беспокоить» вне интервала доступности для отключения уведомлений.", "Save" : "Сохранить", "Failed to load availability" : "Не удалось получить сведения о доступности", + "Saved availability" : "Сведения о доступности сохранены", + "Failed to save availability" : "Не удалось сохранить сведения о доступности", "Calendar server" : "Сервер календаря", "Send invitations to attendees" : "Отправить приглашения", "Automatically generate a birthday calendar" : "Создавать календарь дней рождения автоматически", @@ -153,6 +171,8 @@ "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}.", diff --git a/apps/dav/l10n/sr.js b/apps/dav/l10n/sr.js index 785cbb369f3..fcf7c612d25 100644 --- a/apps/dav/l10n/sr.js +++ b/apps/dav/l10n/sr.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "„%1$s” је отказано", "Re: %1$s" : "Одг: %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:" : "Организатор:", diff --git a/apps/dav/l10n/sr.json b/apps/dav/l10n/sr.json index 9fc2778eb24..a18db47c21a 100644 --- a/apps/dav/l10n/sr.json +++ b/apps/dav/l10n/sr.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "„%1$s” је отказано", "Re: %1$s" : "Одг: %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:" : "Организатор:", diff --git a/apps/dav/l10n/sv.js b/apps/dav/l10n/sv.js index 71c4c10517e..3c3f3ad10cc 100644 --- a/apps/dav/l10n/sv.js +++ b/apps/dav/l10n/sv.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "\"%1$s\" har avbrutits", "Re: %1$s" : "Sv: %1$s", "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s skulle vilja bjuda in dig till \"%2$s\"", "Organizer:" : "Arrangör:", diff --git a/apps/dav/l10n/sv.json b/apps/dav/l10n/sv.json index 65664c4f6a2..b42cda1141b 100644 --- a/apps/dav/l10n/sv.json +++ b/apps/dav/l10n/sv.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "\"%1$s\" har avbrutits", "Re: %1$s" : "Sv: %1$s", "%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", "%1$s would like to invite you to \"%2$s\"" : "%1$s skulle vilja bjuda in dig till \"%2$s\"", "Organizer:" : "Arrangör:", diff --git a/apps/dav/l10n/tr.js b/apps/dav/l10n/tr.js index 88f4d527f24..e6204206c92 100644 --- a/apps/dav/l10n/tr.js +++ b/apps/dav/l10n/tr.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "\"%1$s\" iptal edildi", "Re: %1$s" : "Ynt: %1$s", "%1$s has responded to your invitation" : "%1$s çağrınızı yanıtladı", + "Invitation updated: %1$s" : "Çağrı güncellendi: %1$s", + "%1$s updated the event \"%2$s\"" : "%1$s, \"%2$s\" etkinliğini güncelledi", "Invitation: %1$s" : "Çağrı: %1$s", "%1$s would like to invite you to \"%2$s\"" : "%1$s, size \"%2$s\" için çağrı gönderdi", "Organizer:" : "Düzenleyen:", diff --git a/apps/dav/l10n/tr.json b/apps/dav/l10n/tr.json index 204a5e4e861..08c6cff64cd 100644 --- a/apps/dav/l10n/tr.json +++ b/apps/dav/l10n/tr.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "\"%1$s\" iptal edildi", "Re: %1$s" : "Ynt: %1$s", "%1$s has responded to your invitation" : "%1$s çağrınızı yanıtladı", + "Invitation updated: %1$s" : "Çağrı güncellendi: %1$s", + "%1$s updated the event \"%2$s\"" : "%1$s, \"%2$s\" etkinliğini güncelledi", "Invitation: %1$s" : "Çağrı: %1$s", "%1$s would like to invite you to \"%2$s\"" : "%1$s, size \"%2$s\" için çağrı gönderdi", "Organizer:" : "Düzenleyen:", diff --git a/apps/dav/l10n/zh_HK.js b/apps/dav/l10n/zh_HK.js index d4e18756bd4..36c84b5a31e 100644 --- a/apps/dav/l10n/zh_HK.js +++ b/apps/dav/l10n/zh_HK.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "\"%1$s\" 已被取消", "Re: %1$s" : "關於: %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:" : "主辦單位:", diff --git a/apps/dav/l10n/zh_HK.json b/apps/dav/l10n/zh_HK.json index 7a1bdc824ef..4789f67fda1 100644 --- a/apps/dav/l10n/zh_HK.json +++ b/apps/dav/l10n/zh_HK.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "\"%1$s\" 已被取消", "Re: %1$s" : "關於: %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:" : "主辦單位:", diff --git a/apps/dav/l10n/zh_TW.js b/apps/dav/l10n/zh_TW.js index 38f38a0e42e..ae03ec07516 100644 --- a/apps/dav/l10n/zh_TW.js +++ b/apps/dav/l10n/zh_TW.js @@ -75,6 +75,8 @@ OC.L10N.register( "\"%1$s\" has been canceled" : "「%1$s」已取消", "Re: %1$s" : "回覆:%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:" : "組織者:", diff --git a/apps/dav/l10n/zh_TW.json b/apps/dav/l10n/zh_TW.json index 86fd67cc337..9b612c9022d 100644 --- a/apps/dav/l10n/zh_TW.json +++ b/apps/dav/l10n/zh_TW.json @@ -73,6 +73,8 @@ "\"%1$s\" has been canceled" : "「%1$s」已取消", "Re: %1$s" : "回覆:%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:" : "組織者:", diff --git a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php index 94feadcae93..43fccbf233e 100644 --- a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php +++ b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php @@ -92,7 +92,7 @@ class UserStatusAutomation extends TimedJob { $isCurrentlyAvailable = false; $nextPotentialToggles = []; - $now = new \DateTime('now'); + $now = $this->time->getDateTime(); $lastMidnight = (clone $now)->setTime(0, 0); $vObject = Reader::read($property); @@ -105,9 +105,16 @@ class UserStatusAutomation extends TimedJob { foreach ($availables as $available) { /** @var Available $available */ if ($available->name === 'AVAILABLE') { - /** @var \DateTimeInterface $effectiveStart */ - /** @var \DateTimeInterface $effectiveEnd */ - [$effectiveStart, $effectiveEnd] = $available->getEffectiveStartEnd(); + /** @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); @@ -139,12 +146,21 @@ class UserStatusAutomation extends TimedJob { } } + 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, $argument); + $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); } else { + $this->logger->debug('User is currently NOT available, reverting call status if applicable and then setting DND'); // The DND status automation is more important than the "Away - In call" so we also restore that one if it exists. $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_CALL, IUserStatus::AWAY); $this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); diff --git a/apps/dav/lib/CalDAV/CachedSubscription.php b/apps/dav/lib/CalDAV/CachedSubscription.php index f42b5f97f5d..dc7f66e59b4 100644 --- a/apps/dav/lib/CalDAV/CachedSubscription.php +++ b/apps/dav/lib/CalDAV/CachedSubscription.php @@ -28,7 +28,6 @@ declare(strict_types=1); 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; @@ -38,7 +37,7 @@ use Sabre\DAV\PropPatch; * Class CachedSubscription * * @package OCA\DAV\CalDAV - * @property BackendInterface|CalDavBackend $caldavBackend + * @property CalDavBackend $caldavBackend */ class CachedSubscription extends \Sabre\CalDAV\Calendar { @@ -112,7 +111,7 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar { return parent::getOwner(); } - + public function delete() { $this->caldavBackend->deleteSubscription($this->calendarInfo['id']); } diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index ca79205c09f..76e84a2b54b 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -183,6 +183,7 @@ class IMipPlugin extends SabreIMipPlugin { $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 @@ -255,7 +256,7 @@ class IMipPlugin extends SabreIMipPlugin { $template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data); $template->addHeader(); - $this->imipService->addSubjectAndHeading($template, $method, $data['invitee_name'], $data['meeting_title']); + $this->imipService->addSubjectAndHeading($template, $method, $data['invitee_name'], $data['meeting_title'], $isModified); $this->imipService->addBulletList($template, $vEvent, $data); // Only add response buttons to invitation requests: Fix Issue #11230 diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php index 50e770e6b40..3a624bca4bc 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipService.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php @@ -363,9 +363,10 @@ class IMipService { * @param string $sender * @param string $summary * @param string|null $partstat + * @param bool $isModified */ public function addSubjectAndHeading(IEMailTemplate $template, - string $method, string $sender, string $summary): void { + string $method, string $sender, string $summary, bool $isModified): 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])); @@ -374,6 +375,10 @@ class IMipService { // TRANSLATORS Subject for email, when an invitation is replied to. Ex: "Re: {{Event Name}}" $template->setSubject($this->l10n->t('Re: %1$s', [$summary])); $template->addHeading($this->l10n->t('%1$s has responded to your invitation', [$sender])); + } 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])); diff --git a/apps/dav/lib/CardDAV/AddressBook.php b/apps/dav/lib/CardDAV/AddressBook.php index bca478feec1..f08a7b4b525 100644 --- a/apps/dav/lib/CardDAV/AddressBook.php +++ b/apps/dav/lib/CardDAV/AddressBook.php @@ -38,7 +38,7 @@ 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 { diff --git a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php new file mode 100644 index 00000000000..6c50f5682b7 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php @@ -0,0 +1,133 @@ +<?php +/** + * @copyright Copyright (c) 2023 Claus-Justus Heine + * + * @author Claus-Justus Heine <himself@claus-justus-heine.de> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +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/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index 531ccff9d92..c29070fe921 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -38,6 +38,7 @@ use OC\Metadata\FileMetadata; use OCA\DAV\Connector\Sabre\Exception\FileLocked; use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\DAV\Upload\FutureFile; use OCP\Files\FileInfo; use OCP\Files\Folder; use OCP\Files\ForbiddenException; diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php index ee159cef1d6..2c8d313eefd 100644 --- a/apps/dav/lib/Connector/Sabre/Node.php +++ b/apps/dav/lib/Connector/Sabre/Node.php @@ -261,6 +261,10 @@ abstract class Node implements \Sabre\DAV\INode { return $this->info->getId(); } + public function getInternalPath(): string { + return $this->info->getInternalPath(); + } + /** * @param string $user * @return int diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index a5833e5175f..4be149ac440 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -71,9 +71,11 @@ 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 OCP\AppFramework\Http\Response; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; +use OCP\ICacheFactory; use OCP\IRequest; use OCP\Profiler\IProfiler; use OCP\SabrePluginEvent; @@ -110,6 +112,8 @@ class Server { // Add maintenance plugin $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin(\OC::$server->getConfig(), \OC::$server->getL10N('dav'))); + $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\AppleQuirksPlugin()); + // Backends $authBackend = new Auth( \OC::$server->getSession(), @@ -218,6 +222,7 @@ class Server { $this->server->addPlugin(new CopyEtagHeaderPlugin()); $this->server->addPlugin(new RequestIdHeaderPlugin(\OC::$server->get(IRequest::class))); + $this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class))); $this->server->addPlugin(new ChunkingPlugin()); // allow setup of additional plugins diff --git a/apps/dav/lib/Upload/ChunkingV2Plugin.php b/apps/dav/lib/Upload/ChunkingV2Plugin.php new file mode 100644 index 00000000000..cb7c802125c --- /dev/null +++ b/apps/dav/lib/Upload/ChunkingV2Plugin.php @@ -0,0 +1,392 @@ +<?php + +declare(strict_types=1); +/* + * @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\Upload; + +use Exception; +use InvalidArgumentException; +use OC\Files\Filesystem; +use OC\Files\ObjectStore\ObjectStoreStorage; +use OC\Files\View; +use OC_Hook; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; +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\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(201); + 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 = $storage->free_space($destinationParent->getInternalPath()); + $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 = $storage->free_space($destinationParent->getInternalPath()); + 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 ? 204 : 201); + return false; + } + + public function beforeDelete(RequestInterface $request, ResponseInterface $response) { + try { + $this->prepareUpload($request->getPath()); + if (!$this->uploadFolder instanceof UploadFolder) { + return true; + } + + [$storage, $storagePath] = $this->getUploadStorage($this->uploadPath); + $storage->cancelChunkedWrite($storagePath, $this->uploadId); + return true; + } catch (NotFound $e) { + return true; + } + } + + /** + * @throws BadRequest + * @throws PreconditionFailed + * @throws StorageInvalidException + */ + private function checkPrerequisites(bool $checkUploadMetadata = true): void { + 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 ($checkUploadMetadata) { + if ($this->uploadId === null || $this->uploadPath === null) { + throw new PreconditionFailed('Missing metadata for chunked upload'); + } + } + } + + /** + * @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 = Filesystem::getRoot() . $uploadFile->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/FutureFile.php b/apps/dav/lib/Upload/FutureFile.php index eba550a62da..0b158e364cf 100644 --- a/apps/dav/lib/Upload/FutureFile.php +++ b/apps/dav/lib/Upload/FutureFile.php @@ -36,7 +36,6 @@ use Sabre\DAV\IFile; * @package OCA\DAV\Upload */ class FutureFile implements \Sabre\DAV\IFile { - /** @var Directory */ private $root; /** @var string */ @@ -66,6 +65,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..8bfe992a987 --- /dev/null +++ b/apps/dav/lib/Upload/PartFile.php @@ -0,0 +1,111 @@ +<?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/> + * + */ +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 { + /** @var Directory */ + private $root; + /** @var array */ + private $partInfo; + + public function __construct(Directory $root, array $partInfo) { + $this->root = $root; + $this->partInfo = $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/UploadFile.php b/apps/dav/lib/Upload/UploadFile.php index 023d17955c1..efe1385c8ce 100644 --- a/apps/dav/lib/Upload/UploadFile.php +++ b/apps/dav/lib/Upload/UploadFile.php @@ -44,6 +44,10 @@ class UploadFile implements IFile { return $this->file->get(); } + public function getId() { + return $this->file->getId(); + } + public function getContentType() { return $this->file->getContentType(); } @@ -75,4 +79,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..66c190d84d9 100644 --- a/apps/dav/lib/Upload/UploadFolder.php +++ b/apps/dav/lib/Upload/UploadFolder.php @@ -24,20 +24,25 @@ */ 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 Sabre\DAV\Exception\Forbidden; use Sabre\DAV\ICollection; class UploadFolder implements ICollection { - /** @var Directory */ private $node; /** @var CleanupService */ private $cleanupService; + /** @var IStorage */ + private $storage; - public function __construct(Directory $node, CleanupService $cleanupService) { + public function __construct(Directory $node, CleanupService $cleanupService, IStorage $storage) { $this->node = $node; $this->cleanupService = $cleanupService; + $this->storage = $storage; } public function createFile($name, $data = null) { @@ -66,6 +71,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 = \OC::$server->getMemCacheFactory()->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; } @@ -94,4 +116,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..6664d8c85b6 100644 --- a/apps/dav/lib/Upload/UploadHome.php +++ b/apps/dav/lib/Upload/UploadHome.php @@ -32,7 +32,6 @@ use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\ICollection; class UploadHome implements ICollection { - /** @var array */ private $principalInfo; /** @var CleanupService */ @@ -55,12 +54,12 @@ class UploadHome implements ICollection { } 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()); } public function getChildren(): array { return array_map(function ($node) { - return new UploadFolder($node, $this->cleanupService); + return new UploadFolder($node, $this->cleanupService, $this->getStorage()); }, $this->impl()->getChildren()); } @@ -89,14 +88,24 @@ class UploadHome implements ICollection { * @return Directory */ private function impl() { + $view = $this->getView(); + $rootInfo = $view->getFileInfo(''); + return new Directory($view, $rootInfo); + } + + private function getView() { $rootView = new View(); $user = \OC::$server->getUserSession()->getUser(); Filesystem::initMountPoints($user->getUID()); if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) { $rootView->mkdir('/' . $user->getUID() . '/uploads'); } - $view = new View('/' . $user->getUID() . '/uploads'); - $rootInfo = $view->getFileInfo(''); - return new Directory($view, $rootInfo); + return new View('/' . $user->getUID() . '/uploads'); + } + + private function getStorage() { + $view = $this->getView(); + $storage = $view->getFileInfo('')->getStorage(); + return $storage; } } diff --git a/apps/dav/src/dav/client.js b/apps/dav/src/dav/client.js index ff858e0492c..b053e585ce8 100644 --- a/apps/dav/src/dav/client.js +++ b/apps/dav/src/dav/client.js @@ -1,4 +1,4 @@ -/* +/** * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> * * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at> @@ -21,7 +21,7 @@ import * as webdav from 'webdav' import axios from '@nextcloud/axios' -import memoize from 'lodash/fp/memoize' +import memoize from 'lodash/fp/memoize.js' import { generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' diff --git a/apps/dav/src/service/CalendarService.js b/apps/dav/src/service/CalendarService.js index 2b416d6b670..9faad92f418 100644 --- a/apps/dav/src/service/CalendarService.js +++ b/apps/dav/src/service/CalendarService.js @@ -18,9 +18,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -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/dist/node/tools/dav.js' import { slotsToVavailability, diff --git a/apps/dav/src/settings-personal-availability.js b/apps/dav/src/settings-personal-availability.js index b0d6b19aa8a..8b7bcba0c26 100644 --- a/apps/dav/src/settings-personal-availability.js +++ b/apps/dav/src/settings-personal-availability.js @@ -1,6 +1,6 @@ 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..a99db386d69 100644 --- a/apps/dav/src/settings.js +++ b/apps/dav/src/settings.js @@ -1,7 +1,7 @@ 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 diff --git a/apps/dav/src/views/Availability.vue b/apps/dav/src/views/Availability.vue index e0128a59e0a..bdc0c733c98 100644 --- a/apps/dav/src/views/Availability.vue +++ b/apps/dav/src/views/Availability.vue @@ -47,15 +47,15 @@ import { findScheduleInboxAvailability, getEmptySlots, saveScheduleInboxAvailability, -} from '../service/CalendarService' +} from '../service/CalendarService.js' import { enableUserStatusAutomation, disableUserStatusAutomation, -} from '../service/PreferenceService' -import NcButton from '@nextcloud/vue/dist/Components/NcButton' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch' -import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection' -import NcTimezonePicker from '@nextcloud/vue/dist/Components/NcTimezonePicker' +} from '../service/PreferenceService.js' +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' +import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' +import NcTimezonePicker from '@nextcloud/vue/dist/Components/NcTimezonePicker.js' export default { name: 'Availability', diff --git a/apps/dav/src/views/CalDavSettings.spec.js b/apps/dav/src/views/CalDavSettings.spec.js index 5c81c6259a1..7bc6e2f7b40 100644 --- a/apps/dav/src/views/CalDavSettings.spec.js +++ b/apps/dav/src/views/CalDavSettings.spec.js @@ -1,5 +1,5 @@ import { render } from '@testing-library/vue' -import CalDavSettings from './CalDavSettings' +import CalDavSettings from './CalDavSettings.vue' // eslint-disable-next-line no-unused-vars import { generateUrl } from '@nextcloud/router' diff --git a/apps/dav/src/views/CalDavSettings.vue b/apps/dav/src/views/CalDavSettings.vue index 776f32bff36..6755cbd171d 100644 --- a/apps/dav/src/views/CalDavSettings.vue +++ b/apps/dav/src/views/CalDavSettings.vue @@ -75,8 +75,8 @@ import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import { loadState } from '@nextcloud/initial-state' -import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch' +import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' const userSyncCalendarsDocUrl = loadState('dav', 'userSyncCalendarsDocUrl', '#') diff --git a/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php new file mode 100644 index 00000000000..59438c7cd28 --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php @@ -0,0 +1,204 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\UserStatusAutomation; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +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 MockObject|ITimeFactory $time; + protected MockObject|IJobList $jobList; + protected MockObject|LoggerInterface $logger; + protected MockObject|IManager $statusManager; + protected MockObject|IConfig $config; + + 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); + + } + + protected function getAutomationMock(array $methods): MockObject|UserStatusAutomation { + if (empty($methods)) { + return new UserStatusAutomation( + $this->time, + \OC::$server->getDatabaseConnection(), + $this->jobList, + $this->logger, + $this->statusManager, + $this->config, + ); + } + + return $this->getMockBuilder(UserStatusAutomation::class) + ->setConstructorArgs([ + $this->time, + \OC::$server->getDatabaseConnection(), + $this->jobList, + $this->logger, + $this->statusManager, + $this->config, + ]) + ->setMethods($methods) + ->getMock(); + } + + public 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], + ]; + } + + /** + * @dataProvider dataRun + */ + public function testRun(string $ruleDay, string $currentTime, bool $isAvailable): void { + $this->config->method('getUserValue') + ->with('user', 'dav', 'user_status_automation', 'no') + ->willReturn('yes'); + + $this->time->method('getDateTime') + ->willReturn(new \DateTime($currentTime, new \DateTimeZone('UTC'))); + + $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'); + + if ($isAvailable) { + $this->statusManager->expects($this->once()) + ->method('revertUserStatus') + ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + } else { + $this->statusManager->expects($this->once()) + ->method('revertUserStatus') + ->with('user', IUserStatus::MESSAGE_CALL, IUserStatus::AWAY); + $this->statusManager->expects($this->once()) + ->method('setUserStatus') + ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); + } + + self::invokePrivate($automation, 'run', [['userId' => 'user']]); + } + + public function testRunNoMoreAvailabilityDefined(): void { + $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'))); + + $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 +END:VAVAILABILITY +END:VCALENDAR'); + + $this->statusManager->expects($this->once()) + ->method('revertUserStatus') + ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + + $this->jobList->expects($this->once()) + ->method('remove') + ->with(UserStatusAutomation::class, ['userId' => 'user']); + + self::invokePrivate($automation, 'run', [['userId' => 'user']]); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php index fdd707247ac..bf28fb472a8 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php @@ -204,7 +204,7 @@ class IMipPluginTest extends TestCase { ->method('getFrom'); $this->service->expects(self::once()) ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir'); + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', true); $this->service->expects(self::once()) ->method('addBulletList') ->with($this->emailTemplate, $newVevent, $data); @@ -296,7 +296,7 @@ class IMipPluginTest extends TestCase { ->method('getFrom'); $this->service->expects(self::once()) ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Elevenses'); + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Elevenses', false); $this->service->expects(self::once()) ->method('addBulletList') ->with($this->emailTemplate, $newVevent, $data); @@ -405,7 +405,7 @@ class IMipPluginTest extends TestCase { ->method('getFrom'); $this->service->expects(self::once()) ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir'); + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', false); $this->service->expects(self::once()) ->method('addBulletList') ->with($this->emailTemplate, $newVevent, $data); @@ -480,7 +480,7 @@ class IMipPluginTest extends TestCase { ->method('getFrom'); $this->service->expects(self::once()) ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting'); + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false); $this->service->expects(self::once()) ->method('addBulletList') ->with($this->emailTemplate, $newVevent, $data); @@ -553,7 +553,7 @@ class IMipPluginTest extends TestCase { ->method('getFrom'); $this->service->expects(self::once()) ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting'); + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false); $this->service->expects(self::once()) ->method('addBulletList') ->with($this->emailTemplate, $newVevent, $data); |