aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav')
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php3
-rw-r--r--apps/dav/composer/composer/autoload_static.php3
-rw-r--r--apps/dav/l10n/bg.js2
-rw-r--r--apps/dav/l10n/bg.json2
-rw-r--r--apps/dav/l10n/cs.js4
-rw-r--r--apps/dav/l10n/cs.json4
-rw-r--r--apps/dav/l10n/de.js2
-rw-r--r--apps/dav/l10n/de.json2
-rw-r--r--apps/dav/l10n/de_DE.js4
-rw-r--r--apps/dav/l10n/de_DE.json4
-rw-r--r--apps/dav/l10n/en_GB.js2
-rw-r--r--apps/dav/l10n/en_GB.json2
-rw-r--r--apps/dav/l10n/es.js2
-rw-r--r--apps/dav/l10n/es.json2
-rw-r--r--apps/dav/l10n/eu.js3
-rw-r--r--apps/dav/l10n/eu.json3
-rw-r--r--apps/dav/l10n/fr.js2
-rw-r--r--apps/dav/l10n/fr.json2
-rw-r--r--apps/dav/l10n/gl.js2
-rw-r--r--apps/dav/l10n/gl.json2
-rw-r--r--apps/dav/l10n/hu.js3
-rw-r--r--apps/dav/l10n/hu.json3
-rw-r--r--apps/dav/l10n/pl.js5
-rw-r--r--apps/dav/l10n/pl.json5
-rw-r--r--apps/dav/l10n/pt_BR.js2
-rw-r--r--apps/dav/l10n/pt_BR.json2
-rw-r--r--apps/dav/l10n/ru.js20
-rw-r--r--apps/dav/l10n/ru.json20
-rw-r--r--apps/dav/l10n/sr.js2
-rw-r--r--apps/dav/l10n/sr.json2
-rw-r--r--apps/dav/l10n/sv.js2
-rw-r--r--apps/dav/l10n/sv.json2
-rw-r--r--apps/dav/l10n/tr.js2
-rw-r--r--apps/dav/l10n/tr.json2
-rw-r--r--apps/dav/l10n/zh_HK.js2
-rw-r--r--apps/dav/l10n/zh_HK.json2
-rw-r--r--apps/dav/l10n/zh_TW.js2
-rw-r--r--apps/dav/l10n/zh_TW.json2
-rw-r--r--apps/dav/lib/BackgroundJob/UserStatusAutomation.php24
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscription.php5
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipPlugin.php3
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipService.php7
-rw-r--r--apps/dav/lib/CardDAV/AddressBook.php2
-rw-r--r--apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php133
-rw-r--r--apps/dav/lib/Connector/Sabre/Directory.php1
-rw-r--r--apps/dav/lib/Connector/Sabre/Node.php4
-rw-r--r--apps/dav/lib/Server.php5
-rw-r--r--apps/dav/lib/Upload/ChunkingV2Plugin.php392
-rw-r--r--apps/dav/lib/Upload/FutureFile.php5
-rw-r--r--apps/dav/lib/Upload/PartFile.php111
-rw-r--r--apps/dav/lib/Upload/UploadFile.php16
-rw-r--r--apps/dav/lib/Upload/UploadFolder.php30
-rw-r--r--apps/dav/lib/Upload/UploadHome.php21
-rw-r--r--apps/dav/src/dav/client.js4
-rw-r--r--apps/dav/src/service/CalendarService.js6
-rw-r--r--apps/dav/src/settings-personal-availability.js2
-rw-r--r--apps/dav/src/settings.js2
-rw-r--r--apps/dav/src/views/Availability.vue12
-rw-r--r--apps/dav/src/views/CalDavSettings.spec.js2
-rw-r--r--apps/dav/src/views/CalDavSettings.vue4
-rw-r--r--apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php204
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php10
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);