diff options
Diffstat (limited to 'apps/dav')
85 files changed, 1627 insertions, 118 deletions
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index dd657564ea9..b37e73fa5b6 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -5,7 +5,7 @@ <name>WebDAV</name> <summary>WebDAV endpoint</summary> <description>WebDAV endpoint</description> - <version>1.25.0</version> + <version>1.26.0</version> <licence>agpl</licence> <author>owncloud.org</author> <namespace>DAV</namespace> @@ -15,7 +15,7 @@ <category>integration</category> <bugs>https://github.com/nextcloud/server/issues</bugs> <dependencies> - <nextcloud min-version="26" max-version="26"/> + <nextcloud min-version="27" max-version="27"/> </dependencies> <background-jobs> 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/img/calendar.svg b/apps/dav/img/calendar.svg new file mode 100644 index 00000000000..fed04535dab --- /dev/null +++ b/apps/dav/img/calendar.svg @@ -0,0 +1 @@ +<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="0 0 16 16"><path fill="#000" d="m4 1c-0.5 0-1 0.5-1 1v2c0 0.5 0.5 1 1 1s1-0.5 1-1v-2c0-0.5-0.5-1-1-1zm8 0c-0.5 0-1 0.5-1 1v2c0 0.5 0.5 1 1 1s1-0.5 1-1v-2c0-0.5-0.5-1-1-1zm-6.5 2v1c0 0.831-0.5 1.5-1.5 1.5s-1.5-0.5-1.5-1.5v-0.9375c-0.8841 0.227-1.5 1.0247-1.5 1.9375v8c0 1.108 0.892 2 2 2h10c1.108 0 2-0.892 2-2v-8c0-0.9128-0.61588-1.7105-1.5-1.9375v0.9375c0 0.831-0.5 1.5-1.5 1.5s-1.5-0.5-1.5-1.5v-1zm7.5 5v5h-10v-5z"/></svg> 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/ca.js b/apps/dav/l10n/ca.js index 1174802208c..7a4a3c89a0f 100644 --- a/apps/dav/l10n/ca.js +++ b/apps/dav/l10n/ca.js @@ -72,8 +72,13 @@ OC.L10N.register( "Where: %s" : "On: %s", "%1$s via %2$s" : "%1$s mitjançant %2$s", "Cancelled: %1$s" : "Cancel·lat: %1$s", + "\"%1$s\" has been canceled" : "\"%1$s\" s'ha cancel·lat", "Re: %1$s" : "Re: %1$s", + "%1$s has responded to your invitation" : "%1$s ha respost a la teva invitació", + "Invitation updated: %1$s" : "Invitació actualitzada: %1$s", + "%1$s updated the event \"%2$s\"" : "%1$s ha actualitzat l'esdeveniment \"%2$s\"", "Invitation: %1$s" : "Invitació: %1$s", + "%1$s would like to invite you to \"%2$s\"" : "%1$s vol convidar-vos a \"%2$s\"", "Organizer:" : "Organitzador:", "Attendees:" : "Assistents:", "Title:" : "Títol:", diff --git a/apps/dav/l10n/ca.json b/apps/dav/l10n/ca.json index 0a6f4402503..20db175d8c4 100644 --- a/apps/dav/l10n/ca.json +++ b/apps/dav/l10n/ca.json @@ -70,8 +70,13 @@ "Where: %s" : "On: %s", "%1$s via %2$s" : "%1$s mitjançant %2$s", "Cancelled: %1$s" : "Cancel·lat: %1$s", + "\"%1$s\" has been canceled" : "\"%1$s\" s'ha cancel·lat", "Re: %1$s" : "Re: %1$s", + "%1$s has responded to your invitation" : "%1$s ha respost a la teva invitació", + "Invitation updated: %1$s" : "Invitació actualitzada: %1$s", + "%1$s updated the event \"%2$s\"" : "%1$s ha actualitzat l'esdeveniment \"%2$s\"", "Invitation: %1$s" : "Invitació: %1$s", + "%1$s would like to invite you to \"%2$s\"" : "%1$s vol convidar-vos a \"%2$s\"", "Organizer:" : "Organitzador:", "Attendees:" : "Assistents:", "Title:" : "Títol:", 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 97a417a8609..92a95ba9995 100644 --- a/apps/dav/l10n/fr.js +++ b/apps/dav/l10n/fr.js @@ -37,7 +37,7 @@ OC.L10N.register( "{actor} restored event {event} of calendar {calendar}" : "{actor} a restauré l'événement {event} dans l'agenda {calendar}", "You restored event {event} of calendar {calendar}" : "Vous avez restauré l'événement {event} dans l'agenda {calendar}", "Busy" : "Occupé", - "{actor} created to-do {todo} in list {calendar}" : "{actor} a créé le pense-bête {todo} dans la liste {calendar}", + "{actor} created to-do {todo} in list {calendar}" : "{actor} a créé la tâche {todo} dans la liste {calendar}", "You created to-do {todo} in list {calendar}" : "Vous avez créé un pense-bête {todo} dans la liste {calendar}", "{actor} deleted to-do {todo} from list {calendar}" : "{actor} a supprimé un pense-bête {todo} de la liste {calendar}", "You deleted to-do {todo} from list {calendar}" : "Vous avez supprimé le pense-bête {todo} de la liste {calendar}", @@ -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 :", @@ -85,7 +87,7 @@ OC.L10N.register( "Link:" : "Lien :", "Accept" : "Accepter", "Decline" : "Refuser", - "More options …" : "Plus d'options …", + "More options …" : "Plus d'options…", "More options at %s" : "Plus d'options à %s", "Contacts" : "Contacts", "{actor} created address book {addressbook}" : "{actor} a créé le carnet d'adresses {addressbook}", diff --git a/apps/dav/l10n/fr.json b/apps/dav/l10n/fr.json index f07134e100f..b266d7c3dff 100644 --- a/apps/dav/l10n/fr.json +++ b/apps/dav/l10n/fr.json @@ -35,7 +35,7 @@ "{actor} restored event {event} of calendar {calendar}" : "{actor} a restauré l'événement {event} dans l'agenda {calendar}", "You restored event {event} of calendar {calendar}" : "Vous avez restauré l'événement {event} dans l'agenda {calendar}", "Busy" : "Occupé", - "{actor} created to-do {todo} in list {calendar}" : "{actor} a créé le pense-bête {todo} dans la liste {calendar}", + "{actor} created to-do {todo} in list {calendar}" : "{actor} a créé la tâche {todo} dans la liste {calendar}", "You created to-do {todo} in list {calendar}" : "Vous avez créé un pense-bête {todo} dans la liste {calendar}", "{actor} deleted to-do {todo} from list {calendar}" : "{actor} a supprimé un pense-bête {todo} de la liste {calendar}", "You deleted to-do {todo} from list {calendar}" : "Vous avez supprimé le pense-bête {todo} de la liste {calendar}", @@ -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 :", @@ -83,7 +85,7 @@ "Link:" : "Lien :", "Accept" : "Accepter", "Decline" : "Refuser", - "More options …" : "Plus d'options …", + "More options …" : "Plus d'options…", "More options at %s" : "Plus d'options à %s", "Contacts" : "Contacts", "{actor} created address book {addressbook}" : "{actor} a créé le carnet d'adresses {addressbook}", diff --git a/apps/dav/l10n/gl.js b/apps/dav/l10n/gl.js index 503de28a313..3245bb7bb20 100644 --- a/apps/dav/l10n/gl.js +++ b/apps/dav/l10n/gl.js @@ -2,6 +2,7 @@ OC.L10N.register( "dav", { "Calendar" : "Calendario", + "To-dos" : "Tarefas pendentes", "Personal" : "Persoal", "{actor} created calendar {calendar}" : "{actor} creou o calendario {calendar}", "You created calendar {calendar}" : "Vostede creou o calendario {calendar}", @@ -9,6 +10,8 @@ OC.L10N.register( "You deleted calendar {calendar}" : "Vostede eliminou o calendario {calendar}", "{actor} updated calendar {calendar}" : "{actor} actualizou o calendario {calendar}", "You updated calendar {calendar}" : "Vostede actualizou o calendario {calendar}", + "{actor} restored calendar {calendar}" : "{actor} restaurou o calendario {calendar}", + "You restored calendar {calendar}" : "Restauraches o calendario {calendar}", "You shared calendar {calendar} as public link" : "Vostede compartiu o calendario {calendar} como ligazón pública", "You removed public link for calendar {calendar}" : "Vostede retirou a ligazón pública do calendario {calendar}", "{actor} shared calendar {calendar} with you" : "{actor} compartiu o calendario {calendar} con vostede", @@ -29,9 +32,27 @@ OC.L10N.register( "You deleted event {event} from calendar {calendar}" : "Vostede eliminou o evento {event} do calendario {calendar}", "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizou o evento {event} no calendario {calendar}", "You updated event {event} in calendar {calendar}" : "Vostede actualizou o evento {event} no calendario {calendar}", + "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moveu o evento {event} do calendario {sourceCalendar} ao calendario {targetCalendar}", + "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Moveches o evento {evento} do calendario {sourceCalendar} ao calendario {targetCalendar}", + "{actor} restored event {event} of calendar {calendar}" : "{actor} restaurou o evento {evento} do calendario {calendar}", + "You restored event {event} of calendar {calendar}" : "Restauraches o evento {evento} do calendario {calendar}", "Busy" : "Ocupado", + "{actor} created to-do {todo} in list {calendar}" : "{actor} creou as tarefas {todo} na lista {calendar}", + "You created to-do {todo} in list {calendar}" : "Creaches a tarefa {todo} na lista {calendar}", + "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eliminou as tarefas pendentes {todo} da lista {calendar}", + "You deleted to-do {todo} from list {calendar}" : "Eliminaches as tarefas pendentes {todo} da lista {calendar}", + "{actor} updated to-do {todo} in list {calendar}" : "{actor} actualizou a tarefa {todo} na lista {calendar}", + "You updated to-do {todo} in list {calendar}" : "Actualizaches a tarefa {todo} na lista {calendar}", + "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolveu as tarefas {todo} na lista {calendar}", + "You solved to-do {todo} in list {calendar}" : "Resolveches a tarefa {todo} na lista {calendar}", + "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabriu as tarefas {todo} na lista {calendar}", + "You reopened to-do {todo} in list {calendar}" : "Reabriches as tarefas {todo} na lista {calendar}", + "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moveu a tarefa {todo} da lista {sourceCalendar} á lista {targetCalendar}", + "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Moveches a tarefa {todo} da lista {sourceCalendar} á lista {targetCalendar}", + "Calendar, contacts and tasks" : "Calendario, contactos e tarefas", "A <strong>calendar</strong> was modified" : "Foi modificado un <strong>calendario</strong>", "A calendar <strong>event</strong> was modified" : "Foi modificado un <strong>evento</strong> do calendario", + "A calendar <strong>to-do</strong> was modified" : "Modificouse unha <strong>tarefa</strong> do calendario", "Contact birthdays" : "Aniversario do contacto", "Death of %s" : "Falecemento de %s", "Calendar:" : "Calendario:", @@ -50,6 +71,14 @@ OC.L10N.register( "Description: %s" : "Descrición: %s", "Where: %s" : "Onde: %s", "%1$s via %2$s" : "%1$s mediante %2$s", + "Cancelled: %1$s" : "Cancelado: %1$s", + "\"%1$s\" has been canceled" : "\"%1$s\" cancelouse", + "Re: %1$s" : "Re: %1$s", + "%1$s has responded to your invitation" : "%1$s respondeu á túa invitación", + "Invitation updated: %1$s" : "Invitación actualizada: %1$s", + "%1$s updated the event \"%2$s\"" : "%1$s actualizou o evento \"%2$s\"", + "Invitation: %1$s" : "Invitación: %1$s", + "%1$s would like to invite you to \"%2$s\"" : "%1$s quere invitarte a \"%2$s\"", "Organizer:" : "Organizador:", "Attendees:" : "Asistentes:", "Title:" : "Título:", @@ -61,6 +90,48 @@ OC.L10N.register( "More options …" : "Máis opcións…", "More options at %s" : "Máis opcións en %s", "Contacts" : "Contactos", + "{actor} created address book {addressbook}" : "{actor} creou a axenda de enderezos {addressbook}", + "You created address book {addressbook}" : "Creaches a axenda de enderezos {addressbook}", + "{actor} deleted address book {addressbook}" : "{actor} eliminou a axenda de enderezos {addressbook}", + "You deleted address book {addressbook}" : "Eliminaches a axenda de enderezos {addressbook}", + "{actor} updated address book {addressbook}" : "{actor} actualizou a axenda de enderezos {addressbook}", + "You updated address book {addressbook}" : "Actualizaches a axenda de enderezos {addressbook}", + "{actor} shared address book {addressbook} with you" : "{actor} compartiu a axenda de enderezos {addressbook} contigo", + "You shared address book {addressbook} with {user}" : "Compartiches a axenda de enderezos {addressbook} con {user}", + "{actor} shared address book {addressbook} with {user}" : "{actor} compartiu a axenda de enderezos {addressbook} con {user}", + "{actor} unshared address book {addressbook} from you" : "{actor} deixou de compartir a túa axenda de enderezos {addressbook}", + "You unshared address book {addressbook} from {user}" : "Deixaches de compartir a axenda de enderezos {addressbook} de {user}", + "{actor} unshared address book {addressbook} from {user}" : "{actor} deixou de compartir a axenda de enderezos {addressbook} de {user}", + "{actor} unshared address book {addressbook} from themselves" : "{actor} deixaron de compartir a súa axenda de enderezos {addressbook}", + "You shared address book {addressbook} with group {group}" : "Compartiches a axenda de enderezos {addressbook} co grupo {group}", + "{actor} shared address book {addressbook} with group {group}" : "{actor} compartiu a axenda de enderezos {addressbook} co grupo {group}", + "You unshared address book {addressbook} from group {group}" : "Deixaches de compartir a axenda de enderezos {addressbook} do grupo {group}", + "{actor} unshared address book {addressbook} from group {group}" : "{actor} deixou de compartir axenda de enderezos {addressbook} do grupo {group}", + "{actor} created contact {card} in address book {addressbook}" : "{actor} creou o contacto {card} na axenda de enderezos {addressbook}", + "You created contact {card} in address book {addressbook}" : "Creaches o contacto {card} na axenda de enderezos {addressbook}", + "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eliminou o contacto {card} da axenda de enderezos {addressbook}", + "You deleted contact {card} from address book {addressbook}" : "Eliminaches o contacto {card} da axenda de enderezos {addressbook}", + "{actor} updated contact {card} in address book {addressbook}" : "{actor} actualizou o contacto {card} na axenda de enderezos {addressbook}", + "You updated contact {card} in address book {addressbook}" : "Actualizaches o contacto {card} na axenda de enderezos {addressbook}", + "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Modificouse un <strong>contacto</strong> ou <strong>axenda de enderezos</strong>", + "File is not updatable: %1$s" : "O ficheiro non se pode actualizar: %1$s", + "Could not write to final file, canceled by hook" : "Non foi posíbel escribir no ficheiro final, cancelouse polo hook", + "Could not write file contents" : "Non se puido escribir o contido do ficheiro", + "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"], + "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Produciuse un erro ao copiar o ficheiro na localización de destino (copiado: %1$s, tamaño esperado do ficheiro: %2$s)", + "Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Tamaño esperado do ficheiro %1$s pero lido (do cliente de Nextcloud) e escrito (no almacenamento de Nextcloud) %2$s. Pode ser un problema de rede no lado do envío ou un problema ao escribir no almacenamento no lado do servidor.", + "Could not rename part file to final file, canceled by hook" : "Non foi posíbel cambiar o nome do ficheiro de parte ao ficheiro final, cancelado polo hook", + "Could not rename part file to final file" : "Non se puido cambiar o nome do ficheiro de parte ao ficheiro final", + "Failed to check file size: %1$s" : "Produciuse un erro ao comprobar o tamaño do ficheiro: %1$s", + "Could not open file" : "Non se puido abrir o ficheiro", + "Encryption not ready: %1$s" : "O cifrado non está listo: %1$s", + "Failed to open file: %1$s" : "Produciuse un erro ao abrir o ficheiro: %1$s", + "Failed to unlink: %1$s" : "Produciuse un erro ao desvincular: %1$s", + "Invalid chunk name" : "O nome do fragmento non é válido", + "Could not rename part file assembled from chunks" : "Non se puido cambiar o nome do ficheiro de pezas ensamblados a partir de fragmentos", + "Failed to write file contents: %1$s" : "Produciuse un erro ao escribir o contido do ficheiro: %1$s", + "File not found: %1$s" : "Arquivo non atopado: %1$s", + "System is in maintenance mode." : "O sistema está en modo de mantemento.", "Upgrade needed" : "É necesario anovar actualizar", "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "É preciso configurar o seu %s para que empregue HTTPS para poder usar CalDAV e CardDAV con iOS / macOS.", "Configures a CalDAV account" : "Configurar unha conta de CalDAV", @@ -71,11 +142,18 @@ OC.L10N.register( "Completed on %s" : "Rematado o %s", "Due on %s by %s" : "Caduca o %s por %s", "Due on %s" : "Caduca o %s", + "Migrated calendar (%1$s)" : "Calendario migrado (%1$s)", + "Calendars including events, details and attendees" : "Calendarios incluíndo eventos, detalles e asistentes", "Contacts and groups" : "Contactos e grupos", "WebDAV" : "WebDAV", "WebDAV endpoint" : "Terminación WebDAV", "Availability" : "Dispoñibilidade", + "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Se configuras o teu horario de traballo, outros usuarios verán cando estás fóra da oficina cando reserven unha reunión.", + "Time zone:" : "Franxa horaria:", "to" : "para", + "Delete slot" : "Eliminar slot", + "No working hours set" : "Sen horario de traballo establecido", + "Add slot" : "Engadir slot", "Monday" : "luns", "Tuesday" : "martes", "Wednesday" : "mércores", @@ -83,7 +161,11 @@ OC.L10N.register( "Friday" : "venres", "Saturday" : "sábado", "Sunday" : "domingo", + "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Establece automaticamente o estado do usuario en \"Non molestar\" fóra da dispoñibilidade para silenciar todas as notificacións.", "Save" : "Gardar", + "Failed to load availability" : "Produciuse un erro ao cargar a dispoñibilidade", + "Saved availability" : "Dispoñibilidade gardada", + "Failed to save availability" : "Produciuse un erro ao gardar a dispoñibilidade", "Calendar server" : "Servidor do calendario", "Send invitations to attendees" : "Enviar convites aos asistentes", "Automatically generate a birthday calendar" : "Xerar automaticamente o calendario de aniversarios", @@ -91,7 +173,9 @@ OC.L10N.register( "Hence they will not be available immediately after enabling but will show up after some time." : "Por isto, non estarán dispoñíbeis inmediatamente tras activalos, senón que aparecerán após certo tempo", "Send notifications for events" : "Enviar notificacións para eventos", "Notifications are sent via background jobs, so these must occur often enough." : "As notificacións enviaranse mediante procesos en segundo plano, polo que estes teñen que suceder con frecuencia.", - "Enable notifications for events via push" : "Activar o envío de notificacións do servidor para eventos", + "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 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 bbabb9db7b0..3e8649ff7fb 100644 --- a/apps/dav/l10n/gl.json +++ b/apps/dav/l10n/gl.json @@ -1,5 +1,6 @@ { "translations": { "Calendar" : "Calendario", + "To-dos" : "Tarefas pendentes", "Personal" : "Persoal", "{actor} created calendar {calendar}" : "{actor} creou o calendario {calendar}", "You created calendar {calendar}" : "Vostede creou o calendario {calendar}", @@ -7,6 +8,8 @@ "You deleted calendar {calendar}" : "Vostede eliminou o calendario {calendar}", "{actor} updated calendar {calendar}" : "{actor} actualizou o calendario {calendar}", "You updated calendar {calendar}" : "Vostede actualizou o calendario {calendar}", + "{actor} restored calendar {calendar}" : "{actor} restaurou o calendario {calendar}", + "You restored calendar {calendar}" : "Restauraches o calendario {calendar}", "You shared calendar {calendar} as public link" : "Vostede compartiu o calendario {calendar} como ligazón pública", "You removed public link for calendar {calendar}" : "Vostede retirou a ligazón pública do calendario {calendar}", "{actor} shared calendar {calendar} with you" : "{actor} compartiu o calendario {calendar} con vostede", @@ -27,9 +30,27 @@ "You deleted event {event} from calendar {calendar}" : "Vostede eliminou o evento {event} do calendario {calendar}", "{actor} updated event {event} in calendar {calendar}" : "{actor} actualizou o evento {event} no calendario {calendar}", "You updated event {event} in calendar {calendar}" : "Vostede actualizou o evento {event} no calendario {calendar}", + "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} moveu o evento {event} do calendario {sourceCalendar} ao calendario {targetCalendar}", + "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Moveches o evento {evento} do calendario {sourceCalendar} ao calendario {targetCalendar}", + "{actor} restored event {event} of calendar {calendar}" : "{actor} restaurou o evento {evento} do calendario {calendar}", + "You restored event {event} of calendar {calendar}" : "Restauraches o evento {evento} do calendario {calendar}", "Busy" : "Ocupado", + "{actor} created to-do {todo} in list {calendar}" : "{actor} creou as tarefas {todo} na lista {calendar}", + "You created to-do {todo} in list {calendar}" : "Creaches a tarefa {todo} na lista {calendar}", + "{actor} deleted to-do {todo} from list {calendar}" : "{actor} eliminou as tarefas pendentes {todo} da lista {calendar}", + "You deleted to-do {todo} from list {calendar}" : "Eliminaches as tarefas pendentes {todo} da lista {calendar}", + "{actor} updated to-do {todo} in list {calendar}" : "{actor} actualizou a tarefa {todo} na lista {calendar}", + "You updated to-do {todo} in list {calendar}" : "Actualizaches a tarefa {todo} na lista {calendar}", + "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolveu as tarefas {todo} na lista {calendar}", + "You solved to-do {todo} in list {calendar}" : "Resolveches a tarefa {todo} na lista {calendar}", + "{actor} reopened to-do {todo} in list {calendar}" : "{actor} reabriu as tarefas {todo} na lista {calendar}", + "You reopened to-do {todo} in list {calendar}" : "Reabriches as tarefas {todo} na lista {calendar}", + "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} moveu a tarefa {todo} da lista {sourceCalendar} á lista {targetCalendar}", + "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Moveches a tarefa {todo} da lista {sourceCalendar} á lista {targetCalendar}", + "Calendar, contacts and tasks" : "Calendario, contactos e tarefas", "A <strong>calendar</strong> was modified" : "Foi modificado un <strong>calendario</strong>", "A calendar <strong>event</strong> was modified" : "Foi modificado un <strong>evento</strong> do calendario", + "A calendar <strong>to-do</strong> was modified" : "Modificouse unha <strong>tarefa</strong> do calendario", "Contact birthdays" : "Aniversario do contacto", "Death of %s" : "Falecemento de %s", "Calendar:" : "Calendario:", @@ -48,6 +69,14 @@ "Description: %s" : "Descrición: %s", "Where: %s" : "Onde: %s", "%1$s via %2$s" : "%1$s mediante %2$s", + "Cancelled: %1$s" : "Cancelado: %1$s", + "\"%1$s\" has been canceled" : "\"%1$s\" cancelouse", + "Re: %1$s" : "Re: %1$s", + "%1$s has responded to your invitation" : "%1$s respondeu á túa invitación", + "Invitation updated: %1$s" : "Invitación actualizada: %1$s", + "%1$s updated the event \"%2$s\"" : "%1$s actualizou o evento \"%2$s\"", + "Invitation: %1$s" : "Invitación: %1$s", + "%1$s would like to invite you to \"%2$s\"" : "%1$s quere invitarte a \"%2$s\"", "Organizer:" : "Organizador:", "Attendees:" : "Asistentes:", "Title:" : "Título:", @@ -59,6 +88,48 @@ "More options …" : "Máis opcións…", "More options at %s" : "Máis opcións en %s", "Contacts" : "Contactos", + "{actor} created address book {addressbook}" : "{actor} creou a axenda de enderezos {addressbook}", + "You created address book {addressbook}" : "Creaches a axenda de enderezos {addressbook}", + "{actor} deleted address book {addressbook}" : "{actor} eliminou a axenda de enderezos {addressbook}", + "You deleted address book {addressbook}" : "Eliminaches a axenda de enderezos {addressbook}", + "{actor} updated address book {addressbook}" : "{actor} actualizou a axenda de enderezos {addressbook}", + "You updated address book {addressbook}" : "Actualizaches a axenda de enderezos {addressbook}", + "{actor} shared address book {addressbook} with you" : "{actor} compartiu a axenda de enderezos {addressbook} contigo", + "You shared address book {addressbook} with {user}" : "Compartiches a axenda de enderezos {addressbook} con {user}", + "{actor} shared address book {addressbook} with {user}" : "{actor} compartiu a axenda de enderezos {addressbook} con {user}", + "{actor} unshared address book {addressbook} from you" : "{actor} deixou de compartir a túa axenda de enderezos {addressbook}", + "You unshared address book {addressbook} from {user}" : "Deixaches de compartir a axenda de enderezos {addressbook} de {user}", + "{actor} unshared address book {addressbook} from {user}" : "{actor} deixou de compartir a axenda de enderezos {addressbook} de {user}", + "{actor} unshared address book {addressbook} from themselves" : "{actor} deixaron de compartir a súa axenda de enderezos {addressbook}", + "You shared address book {addressbook} with group {group}" : "Compartiches a axenda de enderezos {addressbook} co grupo {group}", + "{actor} shared address book {addressbook} with group {group}" : "{actor} compartiu a axenda de enderezos {addressbook} co grupo {group}", + "You unshared address book {addressbook} from group {group}" : "Deixaches de compartir a axenda de enderezos {addressbook} do grupo {group}", + "{actor} unshared address book {addressbook} from group {group}" : "{actor} deixou de compartir axenda de enderezos {addressbook} do grupo {group}", + "{actor} created contact {card} in address book {addressbook}" : "{actor} creou o contacto {card} na axenda de enderezos {addressbook}", + "You created contact {card} in address book {addressbook}" : "Creaches o contacto {card} na axenda de enderezos {addressbook}", + "{actor} deleted contact {card} from address book {addressbook}" : "{actor} eliminou o contacto {card} da axenda de enderezos {addressbook}", + "You deleted contact {card} from address book {addressbook}" : "Eliminaches o contacto {card} da axenda de enderezos {addressbook}", + "{actor} updated contact {card} in address book {addressbook}" : "{actor} actualizou o contacto {card} na axenda de enderezos {addressbook}", + "You updated contact {card} in address book {addressbook}" : "Actualizaches o contacto {card} na axenda de enderezos {addressbook}", + "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Modificouse un <strong>contacto</strong> ou <strong>axenda de enderezos</strong>", + "File is not updatable: %1$s" : "O ficheiro non se pode actualizar: %1$s", + "Could not write to final file, canceled by hook" : "Non foi posíbel escribir no ficheiro final, cancelouse polo hook", + "Could not write file contents" : "Non se puido escribir o contido do ficheiro", + "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"], + "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Produciuse un erro ao copiar o ficheiro na localización de destino (copiado: %1$s, tamaño esperado do ficheiro: %2$s)", + "Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Tamaño esperado do ficheiro %1$s pero lido (do cliente de Nextcloud) e escrito (no almacenamento de Nextcloud) %2$s. Pode ser un problema de rede no lado do envío ou un problema ao escribir no almacenamento no lado do servidor.", + "Could not rename part file to final file, canceled by hook" : "Non foi posíbel cambiar o nome do ficheiro de parte ao ficheiro final, cancelado polo hook", + "Could not rename part file to final file" : "Non se puido cambiar o nome do ficheiro de parte ao ficheiro final", + "Failed to check file size: %1$s" : "Produciuse un erro ao comprobar o tamaño do ficheiro: %1$s", + "Could not open file" : "Non se puido abrir o ficheiro", + "Encryption not ready: %1$s" : "O cifrado non está listo: %1$s", + "Failed to open file: %1$s" : "Produciuse un erro ao abrir o ficheiro: %1$s", + "Failed to unlink: %1$s" : "Produciuse un erro ao desvincular: %1$s", + "Invalid chunk name" : "O nome do fragmento non é válido", + "Could not rename part file assembled from chunks" : "Non se puido cambiar o nome do ficheiro de pezas ensamblados a partir de fragmentos", + "Failed to write file contents: %1$s" : "Produciuse un erro ao escribir o contido do ficheiro: %1$s", + "File not found: %1$s" : "Arquivo non atopado: %1$s", + "System is in maintenance mode." : "O sistema está en modo de mantemento.", "Upgrade needed" : "É necesario anovar actualizar", "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "É preciso configurar o seu %s para que empregue HTTPS para poder usar CalDAV e CardDAV con iOS / macOS.", "Configures a CalDAV account" : "Configurar unha conta de CalDAV", @@ -69,11 +140,18 @@ "Completed on %s" : "Rematado o %s", "Due on %s by %s" : "Caduca o %s por %s", "Due on %s" : "Caduca o %s", + "Migrated calendar (%1$s)" : "Calendario migrado (%1$s)", + "Calendars including events, details and attendees" : "Calendarios incluíndo eventos, detalles e asistentes", "Contacts and groups" : "Contactos e grupos", "WebDAV" : "WebDAV", "WebDAV endpoint" : "Terminación WebDAV", "Availability" : "Dispoñibilidade", + "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Se configuras o teu horario de traballo, outros usuarios verán cando estás fóra da oficina cando reserven unha reunión.", + "Time zone:" : "Franxa horaria:", "to" : "para", + "Delete slot" : "Eliminar slot", + "No working hours set" : "Sen horario de traballo establecido", + "Add slot" : "Engadir slot", "Monday" : "luns", "Tuesday" : "martes", "Wednesday" : "mércores", @@ -81,7 +159,11 @@ "Friday" : "venres", "Saturday" : "sábado", "Sunday" : "domingo", + "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Establece automaticamente o estado do usuario en \"Non molestar\" fóra da dispoñibilidade para silenciar todas as notificacións.", "Save" : "Gardar", + "Failed to load availability" : "Produciuse un erro ao cargar a dispoñibilidade", + "Saved availability" : "Dispoñibilidade gardada", + "Failed to save availability" : "Produciuse un erro ao gardar a dispoñibilidade", "Calendar server" : "Servidor do calendario", "Send invitations to attendees" : "Enviar convites aos asistentes", "Automatically generate a birthday calendar" : "Xerar automaticamente o calendario de aniversarios", @@ -89,7 +171,9 @@ "Hence they will not be available immediately after enabling but will show up after some time." : "Por isto, non estarán dispoñíbeis inmediatamente tras activalos, senón que aparecerán após certo tempo", "Send notifications for events" : "Enviar notificacións para eventos", "Notifications are sent via background jobs, so these must occur often enough." : "As notificacións enviaranse mediante procesos en segundo plano, polo que estes teñen que suceder con frecuencia.", - "Enable notifications for events via push" : "Activar o envío de notificacións do servidor para eventos", + "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 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/mk.js b/apps/dav/l10n/mk.js index 024a6fd7f38..b474d61133c 100644 --- a/apps/dav/l10n/mk.js +++ b/apps/dav/l10n/mk.js @@ -131,6 +131,7 @@ OC.L10N.register( "Friday" : "Петок", "Saturday" : "Сабота", "Sunday" : "Недела", + "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматско поставување на статус во \"Не вознемирувај\" недостапен за да ги занемите сите известувања.", "Save" : "Зачувај", "Calendar server" : "Календар сервер", "Send invitations to attendees" : "Испрати покани на учесниците", diff --git a/apps/dav/l10n/mk.json b/apps/dav/l10n/mk.json index 5c4e42f7539..122a5121849 100644 --- a/apps/dav/l10n/mk.json +++ b/apps/dav/l10n/mk.json @@ -129,6 +129,7 @@ "Friday" : "Петок", "Saturday" : "Сабота", "Sunday" : "Недела", + "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматско поставување на статус во \"Не вознемирувај\" недостапен за да ги занемите сите известувања.", "Save" : "Зачувај", "Calendar server" : "Календар сервер", "Send invitations to attendees" : "Испрати покани на учесниците", 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/sl.js b/apps/dav/l10n/sl.js index 5c0f7300594..0214b0aaf7a 100644 --- a/apps/dav/l10n/sl.js +++ b/apps/dav/l10n/sl.js @@ -2,14 +2,17 @@ OC.L10N.register( "dav", { "Calendar" : "Koledar", + "To-dos" : "Naloge To-Do", "Personal" : "Osebno", "{actor} created calendar {calendar}" : "{actor} ustvari koledar {calendar}", - "You created calendar {calendar}" : "Ustvarim koledar {calendar}", + "You created calendar {calendar}" : "Ustvarite koledar {calendar}", "{actor} deleted calendar {calendar}" : "{actor} izbriše koledar {calendar}", - "You deleted calendar {calendar}" : "Izbrišem koledar {calendar}", + "You deleted calendar {calendar}" : "Izbrišete koledar {calendar}", "{actor} updated calendar {calendar}" : "{actor} posodobi koledar {calendar}", - "You updated calendar {calendar}" : "Posodobim koledar {calendar}", - "You shared calendar {calendar} as public link" : "Omogočim souporabo koledarja {calendar} z javno povezavo", + "You updated calendar {calendar}" : "Posodobite koledar {calendar}", + "{actor} restored calendar {calendar}" : "{actor} obnovi koledar {calendar}", + "You restored calendar {calendar}" : "Ustvarite koledar {calendar}", + "You shared calendar {calendar} as public link" : "Omogočite souporabo koledarja {calendar} z javno povezavo", "You removed public link for calendar {calendar}" : "Odstranite javno povezavo koledarja {calendar}", "{actor} shared calendar {calendar} with you" : "{actor} mi omogoči souporabo koledarja {calendar}", "You shared calendar {calendar} with {user}" : "Omogočite souporabo koledarja {calendar} z uporabnikom {user}", @@ -29,6 +32,10 @@ OC.L10N.register( "You deleted event {event} from calendar {calendar}" : "Izbrišete dogodek {event} v koledarju {calendar}", "{actor} updated event {event} in calendar {calendar}" : "{actor} posodobi dogodek {event} v koledarju {calendar}", "You updated event {event} in calendar {calendar}" : "Posodobite dogodek {event} v koledarju {calendar}", + "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} premakne dogodek {event} iz koledarja {sourceCalendar} v koledar {targetCalendar}", + "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Premaknete dogodek {event} iz koledarja {sourceCalendar} v koledar {targetCalendar}", + "{actor} restored event {event} of calendar {calendar}" : "{actor} obnovi dogodek {event} v koledarju {calendar}", + "You restored event {event} of calendar {calendar}" : "Obnovite dogodek {event} v koledarju {calendar}", "Busy" : "Zasedeno", "Calendar, contacts and tasks" : "Koledar, stiki in naloge", "A <strong>calendar</strong> was modified" : "V <strong>koledar</strong> je vpisana sprememba", @@ -67,6 +74,9 @@ OC.L10N.register( "Contacts" : "Stiki", "You deleted address book {addressbook}" : "Izbrišete imenik {addressbook}", "Could not write file contents" : "Ni mogoče zapisati vsebine datoteke", + "Could not rename part file to final file" : "Ni mogoče preimenovati delne datoteke v končno ime.", + "Failed to check file size: %1$s" : "Preverjanje velikosti je spodletelo: %1$s", + "Could not open file" : "Datoteke ni mogoče odpreti", "System is in maintenance mode." : "Sistem je v vzdrževalnem načinu.", "Upgrade needed" : "Zahtevana je posodobitev", "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Za uporabo CalDAV in CardDAV v okoljih iOS/macOS je treba %s nastaviti za uporabo HTTPS.", @@ -107,7 +117,7 @@ OC.L10N.register( "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Namestite tudi {calendarappstoreopen}Koledar{linkclose}, ali pa se povežite z {calendardocopen}namiznim oziroma mobilnim usklajevalnikom ↗{linkclose}.", "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Prepričajte se, da je {emailopen}poštni strežnik{linkclose} pravilno nastavljen.", "There was an error updating your attendance status." : "Prišlo je do napake med posodabljanjem vaše udeležbe.", - "Please contact the organizer directly." : "Z organizatorjem stopite v stik neposredno.", + "Please contact the organizer directly." : "Z organizatorjem stopite neposredno v stik.", "Are you accepting the invitation?" : "Ali želite sprejeti povabilo?", "Tentative" : "Začasno", "Your attendance was updated successfully." : "Vaša prisotnost je uspešno posodobljena.", diff --git a/apps/dav/l10n/sl.json b/apps/dav/l10n/sl.json index 1b8f86a8523..b42aadb7016 100644 --- a/apps/dav/l10n/sl.json +++ b/apps/dav/l10n/sl.json @@ -1,13 +1,16 @@ { "translations": { "Calendar" : "Koledar", + "To-dos" : "Naloge To-Do", "Personal" : "Osebno", "{actor} created calendar {calendar}" : "{actor} ustvari koledar {calendar}", - "You created calendar {calendar}" : "Ustvarim koledar {calendar}", + "You created calendar {calendar}" : "Ustvarite koledar {calendar}", "{actor} deleted calendar {calendar}" : "{actor} izbriše koledar {calendar}", - "You deleted calendar {calendar}" : "Izbrišem koledar {calendar}", + "You deleted calendar {calendar}" : "Izbrišete koledar {calendar}", "{actor} updated calendar {calendar}" : "{actor} posodobi koledar {calendar}", - "You updated calendar {calendar}" : "Posodobim koledar {calendar}", - "You shared calendar {calendar} as public link" : "Omogočim souporabo koledarja {calendar} z javno povezavo", + "You updated calendar {calendar}" : "Posodobite koledar {calendar}", + "{actor} restored calendar {calendar}" : "{actor} obnovi koledar {calendar}", + "You restored calendar {calendar}" : "Ustvarite koledar {calendar}", + "You shared calendar {calendar} as public link" : "Omogočite souporabo koledarja {calendar} z javno povezavo", "You removed public link for calendar {calendar}" : "Odstranite javno povezavo koledarja {calendar}", "{actor} shared calendar {calendar} with you" : "{actor} mi omogoči souporabo koledarja {calendar}", "You shared calendar {calendar} with {user}" : "Omogočite souporabo koledarja {calendar} z uporabnikom {user}", @@ -27,6 +30,10 @@ "You deleted event {event} from calendar {calendar}" : "Izbrišete dogodek {event} v koledarju {calendar}", "{actor} updated event {event} in calendar {calendar}" : "{actor} posodobi dogodek {event} v koledarju {calendar}", "You updated event {event} in calendar {calendar}" : "Posodobite dogodek {event} v koledarju {calendar}", + "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} premakne dogodek {event} iz koledarja {sourceCalendar} v koledar {targetCalendar}", + "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Premaknete dogodek {event} iz koledarja {sourceCalendar} v koledar {targetCalendar}", + "{actor} restored event {event} of calendar {calendar}" : "{actor} obnovi dogodek {event} v koledarju {calendar}", + "You restored event {event} of calendar {calendar}" : "Obnovite dogodek {event} v koledarju {calendar}", "Busy" : "Zasedeno", "Calendar, contacts and tasks" : "Koledar, stiki in naloge", "A <strong>calendar</strong> was modified" : "V <strong>koledar</strong> je vpisana sprememba", @@ -65,6 +72,9 @@ "Contacts" : "Stiki", "You deleted address book {addressbook}" : "Izbrišete imenik {addressbook}", "Could not write file contents" : "Ni mogoče zapisati vsebine datoteke", + "Could not rename part file to final file" : "Ni mogoče preimenovati delne datoteke v končno ime.", + "Failed to check file size: %1$s" : "Preverjanje velikosti je spodletelo: %1$s", + "Could not open file" : "Datoteke ni mogoče odpreti", "System is in maintenance mode." : "Sistem je v vzdrževalnem načinu.", "Upgrade needed" : "Zahtevana je posodobitev", "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Za uporabo CalDAV in CardDAV v okoljih iOS/macOS je treba %s nastaviti za uporabo HTTPS.", @@ -105,7 +115,7 @@ "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Namestite tudi {calendarappstoreopen}Koledar{linkclose}, ali pa se povežite z {calendardocopen}namiznim oziroma mobilnim usklajevalnikom ↗{linkclose}.", "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Prepričajte se, da je {emailopen}poštni strežnik{linkclose} pravilno nastavljen.", "There was an error updating your attendance status." : "Prišlo je do napake med posodabljanjem vaše udeležbe.", - "Please contact the organizer directly." : "Z organizatorjem stopite v stik neposredno.", + "Please contact the organizer directly." : "Z organizatorjem stopite neposredno v stik.", "Are you accepting the invitation?" : "Ali želite sprejeti povabilo?", "Tentative" : "Začasno", "Your attendance was updated successfully." : "Vaša prisotnost je uspešno posodobljena.", diff --git a/apps/dav/l10n/sr.js b/apps/dav/l10n/sr.js index 4cd28fe4b5b..fcf7c612d25 100644 --- a/apps/dav/l10n/sr.js +++ b/apps/dav/l10n/sr.js @@ -2,6 +2,7 @@ OC.L10N.register( "dav", { "Calendar" : "Календар", + "To-dos" : "Обавезе", "Personal" : "Лично", "{actor} created calendar {calendar}" : "{actor} направи календар {calendar}", "You created calendar {calendar}" : "Направили сте календар {calendar}", @@ -9,6 +10,8 @@ OC.L10N.register( "You deleted calendar {calendar}" : "Обрисали сте календар {calendar}", "{actor} updated calendar {calendar}" : "{actor} ажурира календар {calendar}", "You updated calendar {calendar}" : "Ажурирали сте календар {calendar}", + "{actor} restored calendar {calendar}" : "{actor} је обновио календар {calendar}", + "You restored calendar {calendar}" : "Обновили сте календар {calendar}", "You shared calendar {calendar} as public link" : "Поделили сте календар {calendar} као јавну везу", "You removed public link for calendar {calendar}" : "Уклонили сте јавну везу за календар {calendar}", "{actor} shared calendar {calendar} with you" : "{actor} подели календар {calendar} са вама", @@ -29,9 +32,27 @@ OC.L10N.register( "You deleted event {event} from calendar {calendar}" : "Обрисали сте догађај {event} из календара {calendar}", "{actor} updated event {event} in calendar {calendar}" : "{actor} је ажурирао догађај {event} у календару {calendar}", "You updated event {event} in calendar {calendar}" : "Ажурирали сте догађај {event} у календару {calendar}", + "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} је преместио догађај {event} из календара {sourceCalendar} у календар {targetCalendar}", + "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Преместили сте догађај {event} из календара {sourceCalendar} у календар {targetCalendar}", + "{actor} restored event {event} of calendar {calendar}" : "{actor} је обновио догађај {event} календара {calendar}", + "You restored event {event} of calendar {calendar}" : "Обновили сте догађај {event} календара {calendar}", "Busy" : "Заузет/а", + "{actor} created to-do {todo} in list {calendar}" : "{actor} је креирао обавезу {todo} у листи {calendar}", + "You created to-do {todo} in list {calendar}" : "Креирали сте обавезу {todo} у листи {calendar}", + "{actor} deleted to-do {todo} from list {calendar}" : "{actor} је обрисао обавезу {todo} из листе {calendar}", + "You deleted to-do {todo} from list {calendar}" : "обрисали сте обавезу {todo} из листе {calendar}", + "{actor} updated to-do {todo} in list {calendar}" : "{actor} је ажурирао обавезу {todo} у листи {calendar}", + "You updated to-do {todo} in list {calendar}" : "Ажурирали сте обавезу {todo} у листи {calendar}", + "{actor} solved to-do {todo} in list {calendar}" : "{actor} је извршио обавезу {todo} у листи {calendar}", + "You solved to-do {todo} in list {calendar}" : "Извршили сте обавезу {todo} у листи {calendar}", + "{actor} reopened to-do {todo} in list {calendar}" : "{actor} је поново отворио обавезу {todo} у листи {calendar}", + "You reopened to-do {todo} in list {calendar}" : "Поново сте отворили обавезу {todo} у листи {calendar}", + "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} је преместио обавезу {todo} из листе {sourceCalendar} у листу {targetCalendar}", + "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Преместили сте обавезу {todo} из листе {sourceCalendar} у листу {targetCalendar}", + "Calendar, contacts and tasks" : "Календар, контакти и задаци", "A <strong>calendar</strong> was modified" : "<strong>Календар</strong> је измењен", "A calendar <strong>event</strong> was modified" : "<strong>Догађај</strong> из календара је измењен", + "A calendar <strong>to-do</strong> was modified" : "Календар <strong>обавеза</strong> је измењен", "Contact birthdays" : "Рођендани контаката", "Death of %s" : " %s смрт", "Calendar:" : "Календар:", @@ -50,6 +71,14 @@ OC.L10N.register( "Description: %s" : "Опис: %s", "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" : "Одг: %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:" : "Наслов:", @@ -61,6 +90,48 @@ OC.L10N.register( "More options …" : "Још опција…", "More options at %s" : "Још опција на %s", "Contacts" : "Контакти", + "{actor} created address book {addressbook}" : "{actor} је креирао адресар {addressbook}", + "You created address book {addressbook}" : "Креирали сте адресар {addressbook}", + "{actor} deleted address book {addressbook}" : "{actor} је обрисао адресар {addressbook}", + "You deleted address book {addressbook}" : "Обрисали сте адресар {addressbook}", + "{actor} updated address book {addressbook}" : "{actor} је ажурирао адресар {addressbook}", + "You updated address book {addressbook}" : "Ажурирали сте адресар {addressbook}", + "{actor} shared address book {addressbook} with you" : "{actor} је са вама поделио адресар {addressbook}", + "You shared address book {addressbook} with {user}" : "Поделили сте адресар {addressbook} са {user}", + "{actor} shared address book {addressbook} with {user}" : "{actor} је поделио адресар {addressbook} са {user}", + "{actor} unshared address book {addressbook} from you" : "{actor} је уклонио дељење адресара {addressbook} са вама", + "You unshared address book {addressbook} from {user}" : "Уклонили сте дељење адресара {addressbook} са {user}", + "{actor} unshared address book {addressbook} from {user}" : "{actor} је уклонио дељење адресара {addressbook} са {user}", + "{actor} unshared address book {addressbook} from themselves" : "{actor} више не дели адресар {addressbook} са собом", + "You shared address book {addressbook} with group {group}" : "Поделили сте адресар {addressbook} са групом {group}", + "{actor} shared address book {addressbook} with group {group}" : "{actor} је поделио адресар {addressbook} са групом {group}", + "You unshared address book {addressbook} from group {group}" : "Уклонили сте дељење адресара {addressbook} са групом {group}", + "{actor} unshared address book {addressbook} from group {group}" : "{actor} је уклонио дељење адресара {addressbook} са групом {group}", + "{actor} created contact {card} in address book {addressbook}" : "{actor} је креирао контакт {card} у адресару {addressbook}", + "You created contact {card} in address book {addressbook}" : "Креирали сте контакт {card} у адресару {addressbook}", + "{actor} deleted contact {card} from address book {addressbook}" : "{actor} је обрисао контакт {card} из адресара {addressbook}", + "You deleted contact {card} from address book {addressbook}" : "Обрисали сте контакт {card} из адресара {addressbook}", + "{actor} updated contact {card} in address book {addressbook}" : "{actor} је ажурирао контакт {card} у адресару {addressbook}", + "You updated contact {card} in address book {addressbook}" : "Ажурирали сте контакт {card} у адресару {addressbook}", + "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Измењен је <strong>контакт</strong> или <strong>адресар</strong>", + "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 бајтова"], + "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Грешка приликом копирања фајла на циљну локацију (копирано: %1$s, очекивана величина фајла: %2$s)", + "Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Очекивала се величина фајла %1$s, али је (од Nextcloud клијента) прочитано и уписано (у Nextcloud складиште) %2$s. Или може бити мрежни проблем на страни која шаље, или проблем код уписа у складиште на серверу.", + "Could not rename part file to final file, canceled by hook" : "Делимични фајл не може да се преименује у коначни фајл, отказала је кука", + "Could not rename part file to final file" : "Делимични фајл не може да се преименује у коначни фајл", + "Failed to check file size: %1$s" : "Није успела провера величине фајла: %1$s", + "Could not open file" : "Фајл не може да се отвори", + "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." : "Систем је у режиму одржавања.", "Upgrade needed" : "Потребна надградња", "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s мора да буде подешен да користи HTTPS да бисте користи CalDAV и CardDAV са iOS/macOS-ом.", "Configures a CalDAV account" : "Подешава CalDAV налог", @@ -71,9 +142,18 @@ 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 endpoint" : "WebDAV крајња тачка", + "Availability" : "Доступност", + "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Ако подесите своје радне сате, када буду заказивали састанак, остали корисници ће видети када сте ван канцеларије.", + "Time zone:" : "Временска зона:", "to" : "за", + "Delete slot" : "Обриши прорез", + "No working hours set" : "Нису подешени радни сати", + "Add slot" : "Додај прорез", "Monday" : "Понедељак", "Tuesday" : "Уторак", "Wednesday" : "Среда", @@ -81,7 +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" : "Аутоматски изгенериши календар рођендана", @@ -89,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" : "Укључи обавештења за догађаје преко гурања догађаја", "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Такође инсталирајте {calendarappstoreopen}Календар апликацију{linkclose}, или {calendardocopen}повежите Ваш рачунар & мобилни за синхронизацију ↗{linkclose}.", "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Постарајте се да правилно подесите {emailopen}сервер е-поште{linkclose}.", diff --git a/apps/dav/l10n/sr.json b/apps/dav/l10n/sr.json index da5759d5335..a18db47c21a 100644 --- a/apps/dav/l10n/sr.json +++ b/apps/dav/l10n/sr.json @@ -1,5 +1,6 @@ { "translations": { "Calendar" : "Календар", + "To-dos" : "Обавезе", "Personal" : "Лично", "{actor} created calendar {calendar}" : "{actor} направи календар {calendar}", "You created calendar {calendar}" : "Направили сте календар {calendar}", @@ -7,6 +8,8 @@ "You deleted calendar {calendar}" : "Обрисали сте календар {calendar}", "{actor} updated calendar {calendar}" : "{actor} ажурира календар {calendar}", "You updated calendar {calendar}" : "Ажурирали сте календар {calendar}", + "{actor} restored calendar {calendar}" : "{actor} је обновио календар {calendar}", + "You restored calendar {calendar}" : "Обновили сте календар {calendar}", "You shared calendar {calendar} as public link" : "Поделили сте календар {calendar} као јавну везу", "You removed public link for calendar {calendar}" : "Уклонили сте јавну везу за календар {calendar}", "{actor} shared calendar {calendar} with you" : "{actor} подели календар {calendar} са вама", @@ -27,9 +30,27 @@ "You deleted event {event} from calendar {calendar}" : "Обрисали сте догађај {event} из календара {calendar}", "{actor} updated event {event} in calendar {calendar}" : "{actor} је ажурирао догађај {event} у календару {calendar}", "You updated event {event} in calendar {calendar}" : "Ажурирали сте догађај {event} у календару {calendar}", + "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} је преместио догађај {event} из календара {sourceCalendar} у календар {targetCalendar}", + "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Преместили сте догађај {event} из календара {sourceCalendar} у календар {targetCalendar}", + "{actor} restored event {event} of calendar {calendar}" : "{actor} је обновио догађај {event} календара {calendar}", + "You restored event {event} of calendar {calendar}" : "Обновили сте догађај {event} календара {calendar}", "Busy" : "Заузет/а", + "{actor} created to-do {todo} in list {calendar}" : "{actor} је креирао обавезу {todo} у листи {calendar}", + "You created to-do {todo} in list {calendar}" : "Креирали сте обавезу {todo} у листи {calendar}", + "{actor} deleted to-do {todo} from list {calendar}" : "{actor} је обрисао обавезу {todo} из листе {calendar}", + "You deleted to-do {todo} from list {calendar}" : "обрисали сте обавезу {todo} из листе {calendar}", + "{actor} updated to-do {todo} in list {calendar}" : "{actor} је ажурирао обавезу {todo} у листи {calendar}", + "You updated to-do {todo} in list {calendar}" : "Ажурирали сте обавезу {todo} у листи {calendar}", + "{actor} solved to-do {todo} in list {calendar}" : "{actor} је извршио обавезу {todo} у листи {calendar}", + "You solved to-do {todo} in list {calendar}" : "Извршили сте обавезу {todo} у листи {calendar}", + "{actor} reopened to-do {todo} in list {calendar}" : "{actor} је поново отворио обавезу {todo} у листи {calendar}", + "You reopened to-do {todo} in list {calendar}" : "Поново сте отворили обавезу {todo} у листи {calendar}", + "{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} је преместио обавезу {todo} из листе {sourceCalendar} у листу {targetCalendar}", + "You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Преместили сте обавезу {todo} из листе {sourceCalendar} у листу {targetCalendar}", + "Calendar, contacts and tasks" : "Календар, контакти и задаци", "A <strong>calendar</strong> was modified" : "<strong>Календар</strong> је измењен", "A calendar <strong>event</strong> was modified" : "<strong>Догађај</strong> из календара је измењен", + "A calendar <strong>to-do</strong> was modified" : "Календар <strong>обавеза</strong> је измењен", "Contact birthdays" : "Рођендани контаката", "Death of %s" : " %s смрт", "Calendar:" : "Календар:", @@ -48,6 +69,14 @@ "Description: %s" : "Опис: %s", "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" : "Одг: %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:" : "Наслов:", @@ -59,6 +88,48 @@ "More options …" : "Још опција…", "More options at %s" : "Још опција на %s", "Contacts" : "Контакти", + "{actor} created address book {addressbook}" : "{actor} је креирао адресар {addressbook}", + "You created address book {addressbook}" : "Креирали сте адресар {addressbook}", + "{actor} deleted address book {addressbook}" : "{actor} је обрисао адресар {addressbook}", + "You deleted address book {addressbook}" : "Обрисали сте адресар {addressbook}", + "{actor} updated address book {addressbook}" : "{actor} је ажурирао адресар {addressbook}", + "You updated address book {addressbook}" : "Ажурирали сте адресар {addressbook}", + "{actor} shared address book {addressbook} with you" : "{actor} је са вама поделио адресар {addressbook}", + "You shared address book {addressbook} with {user}" : "Поделили сте адресар {addressbook} са {user}", + "{actor} shared address book {addressbook} with {user}" : "{actor} је поделио адресар {addressbook} са {user}", + "{actor} unshared address book {addressbook} from you" : "{actor} је уклонио дељење адресара {addressbook} са вама", + "You unshared address book {addressbook} from {user}" : "Уклонили сте дељење адресара {addressbook} са {user}", + "{actor} unshared address book {addressbook} from {user}" : "{actor} је уклонио дељење адресара {addressbook} са {user}", + "{actor} unshared address book {addressbook} from themselves" : "{actor} више не дели адресар {addressbook} са собом", + "You shared address book {addressbook} with group {group}" : "Поделили сте адресар {addressbook} са групом {group}", + "{actor} shared address book {addressbook} with group {group}" : "{actor} је поделио адресар {addressbook} са групом {group}", + "You unshared address book {addressbook} from group {group}" : "Уклонили сте дељење адресара {addressbook} са групом {group}", + "{actor} unshared address book {addressbook} from group {group}" : "{actor} је уклонио дељење адресара {addressbook} са групом {group}", + "{actor} created contact {card} in address book {addressbook}" : "{actor} је креирао контакт {card} у адресару {addressbook}", + "You created contact {card} in address book {addressbook}" : "Креирали сте контакт {card} у адресару {addressbook}", + "{actor} deleted contact {card} from address book {addressbook}" : "{actor} је обрисао контакт {card} из адресара {addressbook}", + "You deleted contact {card} from address book {addressbook}" : "Обрисали сте контакт {card} из адресара {addressbook}", + "{actor} updated contact {card} in address book {addressbook}" : "{actor} је ажурирао контакт {card} у адресару {addressbook}", + "You updated contact {card} in address book {addressbook}" : "Ажурирали сте контакт {card} у адресару {addressbook}", + "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Измењен је <strong>контакт</strong> или <strong>адресар</strong>", + "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 бајтова"], + "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Грешка приликом копирања фајла на циљну локацију (копирано: %1$s, очекивана величина фајла: %2$s)", + "Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Очекивала се величина фајла %1$s, али је (од Nextcloud клијента) прочитано и уписано (у Nextcloud складиште) %2$s. Или може бити мрежни проблем на страни која шаље, или проблем код уписа у складиште на серверу.", + "Could not rename part file to final file, canceled by hook" : "Делимични фајл не може да се преименује у коначни фајл, отказала је кука", + "Could not rename part file to final file" : "Делимични фајл не може да се преименује у коначни фајл", + "Failed to check file size: %1$s" : "Није успела провера величине фајла: %1$s", + "Could not open file" : "Фајл не може да се отвори", + "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." : "Систем је у режиму одржавања.", "Upgrade needed" : "Потребна надградња", "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "%s мора да буде подешен да користи HTTPS да бисте користи CalDAV и CardDAV са iOS/macOS-ом.", "Configures a CalDAV account" : "Подешава CalDAV налог", @@ -69,9 +140,18 @@ "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 endpoint" : "WebDAV крајња тачка", + "Availability" : "Доступност", + "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Ако подесите своје радне сате, када буду заказивали састанак, остали корисници ће видети када сте ван канцеларије.", + "Time zone:" : "Временска зона:", "to" : "за", + "Delete slot" : "Обриши прорез", + "No working hours set" : "Нису подешени радни сати", + "Add slot" : "Додај прорез", "Monday" : "Понедељак", "Tuesday" : "Уторак", "Wednesday" : "Среда", @@ -79,7 +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" : "Аутоматски изгенериши календар рођендана", @@ -87,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" : "Укључи обавештења за догађаје преко гурања догађаја", "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Такође инсталирајте {calendarappstoreopen}Календар апликацију{linkclose}, или {calendardocopen}повежите Ваш рачунар & мобилни за синхронизацију ↗{linkclose}.", "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Постарајте се да правилно подесите {emailopen}сервер е-поште{linkclose}.", diff --git a/apps/dav/l10n/sv.js b/apps/dav/l10n/sv.js index a837d8cb1a8..3c3f3ad10cc 100644 --- a/apps/dav/l10n/sv.js +++ b/apps/dav/l10n/sv.js @@ -72,8 +72,13 @@ OC.L10N.register( "Where: %s" : "Var: %s", "%1$s via %2$s" : "%1$s via %2$s", "Cancelled: %1$s" : "Avbruten: %1$s", + "\"%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:", "Attendees:" : "Deltagare:", "Title:" : "Titel:", @@ -110,8 +115,12 @@ OC.L10N.register( "You updated contact {card} in address book {addressbook}" : "Du uppdaterade kontakten {card} i adressboken {addressbook}", "A <strong>contact</strong> or <strong>address book</strong> was modified" : "En <strong>kontakt</strong> eller <strong>adressbok</strong> ändrades", "File is not updatable: %1$s" : "Fil kan inte uppdateras: %1$s", + "Could not write to final file, canceled by hook" : "Kunde ej skriva till den slutgiltiga filen, avbröts av en kopplad åtgärd", "Could not write file contents" : "Kunde inte skriva filens innehåll", "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"], + "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Fel vid kopiering av fil till målplats (kopierade: %1$s, förväntad filstorlek: %2$s)", + "Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Förväntad filstorlek på %1$s men läste (från Nextcloud-klienten) och skrev (till Nextcloud-lagringen) %2$s. Kan antingen vara ett nätverksproblem på sändnings-sidan eller problem med att skriva till lagringen på server-sidan.", + "Could not rename part file to final file, canceled by hook" : "Kunde inte byta namn på filfragment till slutgiltigt filnamn, avbröts av en kopplad åtgärd.", "Could not rename part file to final file" : "Kunde inte ändra namn från temporära filen till slutliga filen", "Failed to check file size: %1$s" : "Kunde inte kontrollera filstorleken: %1$s", "Could not open file" : "Kunde inte öppna fil", @@ -144,6 +153,7 @@ OC.L10N.register( "to" : "till", "Delete slot" : "Radera lucka", "No working hours set" : "Inga arbetstimmar satta", + "Add slot" : "Lägg till lucka", "Monday" : "Måndag", "Tuesday" : "Tisdag", "Wednesday" : "Onsdag", @@ -151,7 +161,11 @@ OC.L10N.register( "Friday" : "Fredag", "Saturday" : "Lördag", "Sunday" : "Söndag", + "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Sätt automatiskt användarstatus till \"Stör ej\" utanför tillgängliga tider för att tysta alla notifikationer.", "Save" : "Spara", + "Failed to load availability" : "Misslyckades med att ladda tidsluckor", + "Saved availability" : "Sparade tidslucka", + "Failed to save availability" : "Misslyckades med att spara tidslucka", "Calendar server" : "Kalenderserver", "Send invitations to attendees" : "Skicka inbjudan till deltagare", "Automatically generate a birthday calendar" : "Generera en födelsedagskalender automatiskt", @@ -159,6 +173,8 @@ OC.L10N.register( "Hence they will not be available immediately after enabling but will show up after some time." : "Därför kommer de inte vara tillgängliga direkt efter aktivering men kommer dyka upp efter en tid.", "Send notifications for events" : "Skicka aviseringar för händelser", "Notifications are sent via background jobs, so these must occur often enough." : "Aviseringar skickas genom bakgrundsjobb, så dessa måste ske tillräckligt ofta.", + "Send reminder notifications to calendar sharees as well" : "Skicka även påminnelser till kalenderdeltagare", + "Reminders are always sent to organizers and attendees." : "Påminnelser skickas alltid till arrangörer och deltagare.", "Enable notifications for events via push" : "Aktivera aviseringar för händelser via push", "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installera även {calendarappstoreopen}Kalender-app{linkclose}, eller {calendardocopen}anslut din dator & mobil för synkronisering ↗{linkclose}.", "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Verifiera och säkerställ inställningar för {emailopen}e-postserver{linkclose}.", diff --git a/apps/dav/l10n/sv.json b/apps/dav/l10n/sv.json index 91744d989fb..b42cda1141b 100644 --- a/apps/dav/l10n/sv.json +++ b/apps/dav/l10n/sv.json @@ -70,8 +70,13 @@ "Where: %s" : "Var: %s", "%1$s via %2$s" : "%1$s via %2$s", "Cancelled: %1$s" : "Avbruten: %1$s", + "\"%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:", "Attendees:" : "Deltagare:", "Title:" : "Titel:", @@ -108,8 +113,12 @@ "You updated contact {card} in address book {addressbook}" : "Du uppdaterade kontakten {card} i adressboken {addressbook}", "A <strong>contact</strong> or <strong>address book</strong> was modified" : "En <strong>kontakt</strong> eller <strong>adressbok</strong> ändrades", "File is not updatable: %1$s" : "Fil kan inte uppdateras: %1$s", + "Could not write to final file, canceled by hook" : "Kunde ej skriva till den slutgiltiga filen, avbröts av en kopplad åtgärd", "Could not write file contents" : "Kunde inte skriva filens innehåll", "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"], + "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Fel vid kopiering av fil till målplats (kopierade: %1$s, förväntad filstorlek: %2$s)", + "Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Förväntad filstorlek på %1$s men läste (från Nextcloud-klienten) och skrev (till Nextcloud-lagringen) %2$s. Kan antingen vara ett nätverksproblem på sändnings-sidan eller problem med att skriva till lagringen på server-sidan.", + "Could not rename part file to final file, canceled by hook" : "Kunde inte byta namn på filfragment till slutgiltigt filnamn, avbröts av en kopplad åtgärd.", "Could not rename part file to final file" : "Kunde inte ändra namn från temporära filen till slutliga filen", "Failed to check file size: %1$s" : "Kunde inte kontrollera filstorleken: %1$s", "Could not open file" : "Kunde inte öppna fil", @@ -142,6 +151,7 @@ "to" : "till", "Delete slot" : "Radera lucka", "No working hours set" : "Inga arbetstimmar satta", + "Add slot" : "Lägg till lucka", "Monday" : "Måndag", "Tuesday" : "Tisdag", "Wednesday" : "Onsdag", @@ -149,7 +159,11 @@ "Friday" : "Fredag", "Saturday" : "Lördag", "Sunday" : "Söndag", + "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Sätt automatiskt användarstatus till \"Stör ej\" utanför tillgängliga tider för att tysta alla notifikationer.", "Save" : "Spara", + "Failed to load availability" : "Misslyckades med att ladda tidsluckor", + "Saved availability" : "Sparade tidslucka", + "Failed to save availability" : "Misslyckades med att spara tidslucka", "Calendar server" : "Kalenderserver", "Send invitations to attendees" : "Skicka inbjudan till deltagare", "Automatically generate a birthday calendar" : "Generera en födelsedagskalender automatiskt", @@ -157,6 +171,8 @@ "Hence they will not be available immediately after enabling but will show up after some time." : "Därför kommer de inte vara tillgängliga direkt efter aktivering men kommer dyka upp efter en tid.", "Send notifications for events" : "Skicka aviseringar för händelser", "Notifications are sent via background jobs, so these must occur often enough." : "Aviseringar skickas genom bakgrundsjobb, så dessa måste ske tillräckligt ofta.", + "Send reminder notifications to calendar sharees as well" : "Skicka även påminnelser till kalenderdeltagare", + "Reminders are always sent to organizers and attendees." : "Påminnelser skickas alltid till arrangörer och deltagare.", "Enable notifications for events via push" : "Aktivera aviseringar för händelser via push", "Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installera även {calendarappstoreopen}Kalender-app{linkclose}, eller {calendardocopen}anslut din dator & mobil för synkronisering ↗{linkclose}.", "Please make sure to properly set up {emailopen}the email server{linkclose}." : "Verifiera och säkerställ inställningar för {emailopen}e-postserver{linkclose}.", diff --git a/apps/dav/l10n/tr.js b/apps/dav/l10n/tr.js index 88f4d527f24..a0315fcf32b 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:", @@ -111,7 +113,7 @@ OC.L10N.register( "You deleted contact {card} from address book {addressbook}" : "{addressbook} adres defterinden {card} kişi kartını sildiniz", "{actor} updated contact {card} in address book {addressbook}" : "{actor}, {addressbook} adres defterindeki {card} kişi kartını güncelledi", "You updated contact {card} in address book {addressbook}" : "{addressbook} adres defterindeki {card} kişi kartını güncellediniz", - "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Bir <strong>kişi</strong> ya da <strong>adres defteri</strong> değiştirildi", + "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Bir <strong>kişi</strong> ya da <strong>adres defteri</strong> değiştirildiğinde", "File is not updatable: %1$s" : "Dosya güncellenebilir değil: %1$s", "Could not write to final file, canceled by hook" : "Sonuç dosyasına yazılamadı, bağlantı tarafından iptal edildi", "Could not write file contents" : "Dosya içerikleri yazılamadı", @@ -145,7 +147,7 @@ OC.L10N.register( "Contacts and groups" : "Kişiler ve gruplar", "WebDAV" : "WebDAV", "WebDAV endpoint" : "WebDAV bağlantı noktası", - "Availability" : "Kullanılabilirlik", + "Availability" : "Uygunluk", "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Çalışma saatlerinizi ayarlarsanız, diğer kullanıcılar bir toplantı ayarladıklarında ofis dışında olduğunuzu görürler.", "Time zone:" : "Saat dilimi:", "to" : "ile", @@ -165,7 +167,7 @@ OC.L10N.register( "Saved availability" : "Uygunluk kaydedildi", "Failed to save availability" : "Uygunluk kaydedilemedi", "Calendar server" : "Takvim sunucusu", - "Send invitations to attendees" : "Katılımcılara çağrıları gönder", + "Send invitations to attendees" : "Katılımcılara çağrılar gönderilsin", "Automatically generate a birthday calendar" : "Doğum günü takvimi otomatik oluşturulsun", "Birthday calendars will be generated by a background job." : "Bu seçenek etkinleştirildiğinde, doğum günü takvimi arka plan görevi olarak oluşturulur.", "Hence they will not be available immediately after enabling but will show up after some time." : "Etkinleştirildikten hemen sonra görüntülenmez, bir süre sonra görüntülenir.", diff --git a/apps/dav/l10n/tr.json b/apps/dav/l10n/tr.json index 204a5e4e861..32c46b864b1 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:", @@ -109,7 +111,7 @@ "You deleted contact {card} from address book {addressbook}" : "{addressbook} adres defterinden {card} kişi kartını sildiniz", "{actor} updated contact {card} in address book {addressbook}" : "{actor}, {addressbook} adres defterindeki {card} kişi kartını güncelledi", "You updated contact {card} in address book {addressbook}" : "{addressbook} adres defterindeki {card} kişi kartını güncellediniz", - "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Bir <strong>kişi</strong> ya da <strong>adres defteri</strong> değiştirildi", + "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Bir <strong>kişi</strong> ya da <strong>adres defteri</strong> değiştirildiğinde", "File is not updatable: %1$s" : "Dosya güncellenebilir değil: %1$s", "Could not write to final file, canceled by hook" : "Sonuç dosyasına yazılamadı, bağlantı tarafından iptal edildi", "Could not write file contents" : "Dosya içerikleri yazılamadı", @@ -143,7 +145,7 @@ "Contacts and groups" : "Kişiler ve gruplar", "WebDAV" : "WebDAV", "WebDAV endpoint" : "WebDAV bağlantı noktası", - "Availability" : "Kullanılabilirlik", + "Availability" : "Uygunluk", "If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Çalışma saatlerinizi ayarlarsanız, diğer kullanıcılar bir toplantı ayarladıklarında ofis dışında olduğunuzu görürler.", "Time zone:" : "Saat dilimi:", "to" : "ile", @@ -163,7 +165,7 @@ "Saved availability" : "Uygunluk kaydedildi", "Failed to save availability" : "Uygunluk kaydedilemedi", "Calendar server" : "Takvim sunucusu", - "Send invitations to attendees" : "Katılımcılara çağrıları gönder", + "Send invitations to attendees" : "Katılımcılara çağrılar gönderilsin", "Automatically generate a birthday calendar" : "Doğum günü takvimi otomatik oluşturulsun", "Birthday calendars will be generated by a background job." : "Bu seçenek etkinleştirildiğinde, doğum günü takvimi arka plan görevi olarak oluşturulur.", "Hence they will not be available immediately after enabling but will show up after some time." : "Etkinleştirildikten hemen sonra görüntülenmez, bir süre sonra görüntülenir.", diff --git a/apps/dav/l10n/uk.js b/apps/dav/l10n/uk.js index 94e7d68c1b8..b434ae4fc88 100644 --- a/apps/dav/l10n/uk.js +++ b/apps/dav/l10n/uk.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:" : "Організатор:", @@ -152,14 +154,14 @@ OC.L10N.register( "Delete slot" : "Вилучити діапазон", "No working hours set" : "Робочий час не встановлено", "Add slot" : "Додати діапазон", - "Monday" : "понеділок", + "Monday" : "Понеділок", "Tuesday" : "Вівторок", "Wednesday" : "Середа", "Thursday" : "Четвер", "Friday" : "П'ятниця", "Saturday" : "Субота", "Sunday" : "Неділя", - "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматично встановлюйте статус користувача на \"Не турбувати\" поза доступністю, щоб вимкнути всі сповіщення.", + "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматично встановлюйте статус користувача на \"Не турбувати\", коли ви не доступні, щоб вимкнути усі сповіщення.", "Save" : "Зберегти", "Failed to load availability" : "Не вдалося завантажити доступність", "Saved availability" : "Збережена наявність", diff --git a/apps/dav/l10n/uk.json b/apps/dav/l10n/uk.json index 74fffa4bda4..6ba6c31dbfe 100644 --- a/apps/dav/l10n/uk.json +++ b/apps/dav/l10n/uk.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:" : "Організатор:", @@ -150,14 +152,14 @@ "Delete slot" : "Вилучити діапазон", "No working hours set" : "Робочий час не встановлено", "Add slot" : "Додати діапазон", - "Monday" : "понеділок", + "Monday" : "Понеділок", "Tuesday" : "Вівторок", "Wednesday" : "Середа", "Thursday" : "Четвер", "Friday" : "П'ятниця", "Saturday" : "Субота", "Sunday" : "Неділя", - "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматично встановлюйте статус користувача на \"Не турбувати\" поза доступністю, щоб вимкнути всі сповіщення.", + "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Автоматично встановлюйте статус користувача на \"Не турбувати\", коли ви не доступні, щоб вимкнути усі сповіщення.", "Save" : "Зберегти", "Failed to load availability" : "Не вдалося завантажити доступність", "Saved availability" : "Збережена наявність", 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/EventComparisonService.php b/apps/dav/lib/CalDAV/EventComparisonService.php index 0fd4d08e83e..d8d6ea07ed2 100644 --- a/apps/dav/lib/CalDAV/EventComparisonService.php +++ b/apps/dav/lib/CalDAV/EventComparisonService.php @@ -90,7 +90,7 @@ class EventComparisonService { * * @param VCalendar $new * @param VCalendar|null $old - * @return array<string, VEvent[]> + * @return array<string, VEvent[]|null> */ public function findModified(VCalendar $new, ?VCalendar $old): array { $newEventComponents = $new->getComponents(); diff --git a/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php b/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php index 19c72ffa0e9..e48e283484c 100644 --- a/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php +++ b/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php @@ -34,6 +34,8 @@ use OCP\IDBConnection; * Class ProxyMapper * * @package OCA\DAV\CalDAV\Proxy + * + * @template-extends QBMapper<Proxy> */ class ProxyMapper extends QBMapper { public const PERMISSION_READ = 1; diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index d4c2976fc1a..76e84a2b54b 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -173,7 +173,7 @@ class IMipPlugin extends SabreIMipPlugin { $iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; return; } - $recipientName = $iTipMessage->recipientName ?: null; + $recipientName = $iTipMessage->recipientName ? (string)$iTipMessage->recipientName : null; $newEvents = $iTipMessage->message; $oldEvents = $this->getVCalendar(); @@ -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 @@ -209,9 +210,11 @@ class IMipPlugin extends SabreIMipPlugin { $senderName = $senderName->getValue() ?? null; } - if ($senderName === null || empty(trim($senderName))) { + // Try to get the sender name from the current user id if available. + if ($this->userId !== null && ($senderName === null || empty(trim($senderName)))) { $senderName = $this->userManager->getDisplayName($this->userId); } + $sender = substr($iTipMessage->sender, 7); switch (strtolower($iTipMessage->method)) { @@ -229,7 +232,6 @@ class IMipPlugin extends SabreIMipPlugin { break; } - $data['attendee_name'] = ($recipientName ?: $recipient); $data['invitee_name'] = ($senderName ?: $sender); @@ -237,14 +239,24 @@ class IMipPlugin extends SabreIMipPlugin { $fromName = $this->imipService->getFrom($senderName, $this->defaults->getName()); $message = $this->mailer->createMessage() - ->setFrom([$fromEMail => $fromName]) - ->setTo([$recipient => $recipientName]) - ->setReplyTo([$sender => $senderName]); + ->setFrom([$fromEMail => $fromName]); + + if ($recipientName !== null) { + $message->setTo([$recipient => $recipientName]); + } else { + $message->setTo([$recipient]); + } + + if ($senderName !== null) { + $message->setReplyTo([$sender => $senderName]); + } else { + $message->setReplyTo([$sender]); + } $template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data); $template->addHeader(); - $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 3e8e72bd2e4..034a59a98d4 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipService.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php @@ -70,11 +70,15 @@ class IMipService { } /** - * @param string $senderName - * @param $default + * @param string|null $senderName + * @param string $default * @return string */ - public function getFrom(string $senderName, $default): string { + public function getFrom(?string $senderName, string $default): string { + if ($senderName === null) { + return $default; + } + return $this->l10n->t('%1$s via %2$s', [$senderName, $default]); } @@ -94,7 +98,7 @@ class IMipService { return $default; } $newstring = $vevent->$property->getValue(); - if(isset($oldVEvent->$property)) { + if(isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring ) { $oldstring = $oldVEvent->$property->getValue(); return sprintf($strikethrough, $oldstring, $newstring); } @@ -124,7 +128,7 @@ class IMipService { $data['meeting_location_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']); $oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal); - $data['meeting_url_html'] = !empty($oldUrl) ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url']; + $data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url']; $data['meeting_when_html'] = ($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null) @@ -359,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])); @@ -370,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])); @@ -468,7 +477,7 @@ class IMipService { */ public function addBulletList(IEMailTemplate $template, VEvent $vevent, $data) { $template->addBodyListItem( - $data['meeting_title'], $this->l10n->t('Title:'), + $data['meeting_title_html'] ?? $data['meeting_title'], $this->l10n->t('Title:'), $this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT); if ($data['meeting_when'] !== '') { $template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Time:'), diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php index ac8521acfee..0751638b697 100644 --- a/apps/dav/lib/CalDAV/Schedule/Plugin.php +++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php @@ -48,7 +48,6 @@ use Sabre\VObject\Component; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VEvent; use Sabre\VObject\DateTimeParser; -use Sabre\VObject\Document; use Sabre\VObject\FreeBusyGenerator; use Sabre\VObject\ITip; use Sabre\VObject\Parameter; @@ -329,12 +328,12 @@ EOF; /** @var CalendarHome $calendarHome */ $calendarHome = $this->server->tree->getNodeForPath($calendarHomePath); - if (!$calendarHome->childExists($uri)) { + $currentCalendarDeleted = false; + if (!$calendarHome->childExists($uri) || $currentCalendarDeleted = $this->isCalendarDeleted($calendarHome, $uri)) { // If the default calendar doesn't exist if ($isResourceOrRoom) { - $calendarHome->getCalDAVBackend()->createCalendar($principalUrl, $uri, [ - '{DAV:}displayname' => $displayName, - ]); + // Resources or rooms can't be in the trashbin, so we're fine + $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName); } else { // And we're not handling scheduling on resource/room booking $userCalendars = []; @@ -359,9 +358,16 @@ EOF; $uri = $userCalendars[0]->getName(); } else { // Otherwise if we have really nothing, create a new calendar - $calendarHome->getCalDAVBackend()->createCalendar($principalUrl, $uri, [ - '{DAV:}displayname' => $displayName, - ]); + if ($currentCalendarDeleted) { + // If the calendar exists but is deleted, we need to purge it first + // This may cause some issues in a non synchronous database setup + $calendar = $this->getCalendar($calendarHome, $uri); + if ($calendar instanceof Calendar) { + $calendar->disableTrashbin(); + $calendar->delete(); + } + } + $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName); } } } @@ -609,4 +615,19 @@ EOF; return $email; } + + private function getCalendar(CalendarHome $calendarHome, string $uri): INode { + return $calendarHome->getChild($uri); + } + + private function isCalendarDeleted(CalendarHome $calendarHome, string $uri): bool { + $calendar = $this->getCalendar($calendarHome, $uri); + return $calendar instanceof Calendar && $calendar->isDeleted(); + } + + private function createCalendar(CalendarHome $calendarHome, string $principalUri, string $uri, string $displayName): void { + $calendarHome->getCalDAVBackend()->createCalendar($principalUri, $uri, [ + '{DAV:}displayname' => $displayName, + ]); + } } 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/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index ab21af1ce10..666f1e7a85c 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -989,11 +989,12 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @param string $pattern which should match within the $searchProperties * @param array $searchProperties defines the properties within the query pattern should match * @param array $options = array() to define the search behavior + * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array * - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are * - 'limit' - Set a numeric limit for the search results * - 'offset' - Set the offset for the limited search results * - 'wildcard' - Whether the search should use wildcards - * @psalm-param array{escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options + * @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options * @return array an array of contacts which are arrays of key-value-pairs */ public function search($addressBookId, $pattern, $searchProperties, $options = []): array { 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 f4b1ee62190..c29070fe921 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -35,10 +35,10 @@ namespace OCA\DAV\Connector\Sabre; use OC\Files\Mount\MoveableMount; use OC\Files\View; use OC\Metadata\FileMetadata; -use OC\Metadata\MetadataGroup; use OCA\DAV\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; @@ -57,7 +57,6 @@ use Sabre\DAV\INode; use OCP\Share\IManager as IShareManager; class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget { - /** * Cached directory content * @var \OCP\Files\FileInfo[] @@ -116,7 +115,6 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol // for chunked upload also updating a existing file is a "createFile" // because we create all the chunks before re-assemble them to the existing file. if (isset($_SERVER['HTTP_OC_CHUNKED'])) { - // exit if we can't create a new file and we don't updatable existing file $chunkInfo = \OC_FileChunking::decodeName($name); if (!$this->fileView->isCreatable($this->path) && @@ -328,8 +326,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol if ($this->quotaInfo) { return $this->quotaInfo; } + $relativePath = $this->fileView->getRelativePath($this->info->getPath()); + if ($relativePath === null) { + $logger->warning("error while getting quota as the relative path cannot be found"); + return [0, 0]; + } + try { - $storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $this->info, false); + $storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false); if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) { $free = \OCP\Files\FileInfo::SPACE_UNLIMITED; } else { diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index a6c9b8b4ebe..6b6f622a5a7 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -436,7 +436,7 @@ class FilesPlugin extends ServerPlugin { \OC::$server->get(LoggerInterface::class)->debug('Inefficient fetching of metadata'); } - return json_encode((object)$sizeMetadata->getMetadata(), JSON_THROW_ON_ERROR); + return $sizeMetadata->getValue(); }); } } 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/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php index 9fa6775c3b5..3d52a44b6a6 100644 --- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php @@ -110,6 +110,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { IShare::TYPE_ROOM, IShare::TYPE_CIRCLE, IShare::TYPE_DECK, + IShare::TYPE_SCIENCEMESH, ]; foreach ($requestedShareTypes as $requestedShareType) { $shares = $this->shareManager->getSharesBy( diff --git a/apps/dav/lib/DAV/CustomPropertiesBackend.php b/apps/dav/lib/DAV/CustomPropertiesBackend.php index 0110990a408..3bc3ba33173 100644 --- a/apps/dav/lib/DAV/CustomPropertiesBackend.php +++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php @@ -98,7 +98,7 @@ class CustomPropertiesBackend implements BackendInterface { /** * Properties set by one user, readable by all others * - * @var array[] + * @var string[] */ private const PUBLISHED_READ_ONLY_PROPERTIES = [ '{urn:ietf:params:xml:ns:caldav}calendar-availability', diff --git a/apps/dav/lib/Events/CalendarShareUpdatedEvent.php b/apps/dav/lib/Events/CalendarShareUpdatedEvent.php index d5a568d149b..dedd9f8a566 100644 --- a/apps/dav/lib/Events/CalendarShareUpdatedEvent.php +++ b/apps/dav/lib/Events/CalendarShareUpdatedEvent.php @@ -38,7 +38,7 @@ use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; class CalendarShareUpdatedEvent extends Event { private int $calendarId; - /** @var array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp } */ + /** @var array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string } */ private array $calendarData; /** @var list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> */ @@ -54,7 +54,7 @@ class CalendarShareUpdatedEvent extends Event { * CalendarShareUpdatedEvent constructor. * * @param int $calendarId - * @param array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp } $calendarData + * @param array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string } $calendarData * @param list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> $oldShares * @param list<array{href: string, commonName: string, readOnly: bool}> $added * @param list<string> $removed @@ -81,7 +81,7 @@ class CalendarShareUpdatedEvent extends Event { } /** - * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp } + * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string } * @since 20.0.0 */ public function getCalendarData(): array { diff --git a/apps/dav/lib/Migration/RemoveObjectProperties.php b/apps/dav/lib/Migration/RemoveObjectProperties.php index c72dfbebfea..b771b70e684 100644 --- a/apps/dav/lib/Migration/RemoveObjectProperties.php +++ b/apps/dav/lib/Migration/RemoveObjectProperties.php @@ -57,7 +57,7 @@ class RemoveObjectProperties implements IRepairStep { $query = $this->connection->getQueryBuilder(); $updated = $query->delete('properties') ->where($query->expr()->in('propertyname', $query->createNamedParameter([self::RESOURCE_TYPE_PROPERTY, self::ME_CARD_PROPERTY, self::CALENDAR_TRANSP_PROPERTY], IQueryBuilder::PARAM_STR_ARRAY))) - ->andWhere($query->expr()->eq('propertyvalue', $query->createNamedParameter('Object'))) + ->andWhere($query->expr()->eq('propertyvalue', $query->createNamedParameter('Object'), IQueryBuilder::PARAM_STR)) ->executeStatement(); $output->info("$updated invalid object properties removed."); diff --git a/apps/dav/lib/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..46c92436d6b 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' 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); diff --git a/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php index 4845188bc88..8f315eac0ee 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php @@ -197,6 +197,16 @@ class PluginTest extends TestCase { false, CalDavBackend::PERSONAL_CALENDAR_URI, CalDavBackend::PERSONAL_CALENDAR_NAME, + true, + true + ], + [ + 'principals/users/myuser', + 'calendars/myuser', + false, + CalDavBackend::PERSONAL_CALENDAR_URI, + CalDavBackend::PERSONAL_CALENDAR_NAME, + false, false, true ], @@ -225,6 +235,7 @@ class PluginTest extends TestCase { true, false, false, + false, ], [ 'principals/users/myuser', @@ -263,16 +274,8 @@ class PluginTest extends TestCase { /** * @dataProvider propFindDefaultCalendarUrlProvider - * @param string $principalUri - * @param string|null $calendarHome - * @param bool $isResource - * @param string $calendarUri - * @param string $displayName - * @param bool $exists - * @param bool $propertiesForPath */ - public function testPropFindDefaultCalendarUrl(string $principalUri, ?string $calendarHome, bool $isResource, string $calendarUri, string $displayName, bool $exists, bool $hasExistingCalendars = false, bool $propertiesForPath = true): void { - /** @var PropFind $propFind */ + public function testPropFindDefaultCalendarUrl(string $principalUri, ?string $calendarHome, bool $isResource, string $calendarUri, string $displayName, bool $exists, bool $deleted = false, bool $hasExistingCalendars = false, bool $propertiesForPath = true): void { $propFind = new PropFind( $principalUri, [ @@ -328,6 +331,12 @@ class PluginTest extends TestCase { ->with($calendarUri) ->willReturn($exists); + if ($exists) { + $calendar = $this->createMock(Calendar::class); + $calendar->expects($this->once())->method('isDeleted')->willReturn($deleted); + $calendarHomeObject->expects($deleted && !$hasExistingCalendars ? $this->exactly(2) : $this->once())->method('getChild')->with($calendarUri)->willReturn($calendar); + } + $calendarBackend = $this->createMock(CalDavBackend::class); $calendarUri = $hasExistingCalendars ? 'custom' : $calendarUri; $displayName = $hasExistingCalendars ? 'Custom Calendar' : $displayName; @@ -349,7 +358,7 @@ class PluginTest extends TestCase { ) ] : []; - if (!$exists) { + if (!$exists || $deleted) { if (!$hasExistingCalendars) { $calendarBackend->expects($this->once()) ->method('createCalendar') diff --git a/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php index edbe4278c3a..c6365cf3168 100644 --- a/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php @@ -63,7 +63,7 @@ class TestViewDirectory extends \OC\Files\View { return $this->canRename; } - public function getRelativePath($path) { + public function getRelativePath($path): ?string { return $path; } } @@ -73,7 +73,6 @@ class TestViewDirectory extends \OC\Files\View { * @group DB */ class DirectoryTest extends \Test\TestCase { - use UserTrait; /** @var \OC\Files\View | \PHPUnit\Framework\MockObject\MockObject */ @@ -304,6 +303,10 @@ class DirectoryTest extends \Test\TestCase { ->method('free_space') ->willReturn(800); + $this->info->expects($this->any()) + ->method('getPath') + ->willReturn('/admin/files/foo'); + $this->info->expects($this->once()) ->method('getSize') ->willReturn(200); @@ -312,6 +315,10 @@ class DirectoryTest extends \Test\TestCase { ->method('getMountPoint') ->willReturn($mountPoint); + $this->view->expects($this->any()) + ->method('getRelativePath') + ->willReturn('/foo'); + $mountPoint->method('getMountPoint') ->willReturn('/user/files/mymountpoint'); @@ -359,6 +366,10 @@ class DirectoryTest extends \Test\TestCase { $mountPoint->method('getMountPoint') ->willReturn('/user/files/mymountpoint'); + $this->view->expects($this->any()) + ->method('getRelativePath') + ->willReturn('/foo'); + $dir = new Directory($this->view, $this->info); $this->assertEquals([200, 800], $dir->getQuotaInfo()); //200 used, 800 free } diff --git a/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php index d219888ef15..8d6bfc1764b 100644 --- a/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php @@ -62,8 +62,7 @@ class ObjectTreeTest extends \Test\TestCase { $view = $this->createMock(View::class); $view->expects($this->once()) ->method('verifyPath') - ->with($targetParent) - ->willReturn(true); + ->with($targetParent); $view->expects($this->once()) ->method('file_exists') ->with($targetPath) diff --git a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php index abbf13d5479..e6e90838966 100644 --- a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php @@ -278,6 +278,7 @@ class SharesPluginTest extends \Test\TestCase { [[IShare::TYPE_REMOTE]], [[IShare::TYPE_ROOM]], [[IShare::TYPE_DECK]], + [[IShare::TYPE_SCIENCEMESH]], [[IShare::TYPE_USER, IShare::TYPE_GROUP]], [[IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK]], [[IShare::TYPE_USER, IShare::TYPE_LINK]], |