Compare commits

..

9 Commits

Author SHA1 Message Date
Josh 6bc9c3c2dd chore(View): drop unnecessary comments from getFileInfo refctor
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-15 10:18:37 -04:00
Josh a701c003ad docs(View): owner lazy load possibility
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-15 10:02:09 -04:00
Josh 57138214e2 refactor(View): improve data/ICacheEntry mutation logic in getFileInfo
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-15 09:49:51 -04:00
Josh 2d993ee091 refactor(View): correct outdated getOwner context in getFileInfo
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-15 09:11:59 -04:00
Josh 150e13059e refactor(View): make partial file handling in getFileInfo more explicit
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-15 01:07:11 -04:00
Josh 3a45fa1c63 fix(View): handle valid but "truthy" names robustly in getFileInfo
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-15 00:55:19 -04:00
Josh 7978ed0cfc refactor(View): mount/storage null-safety + related logging in getFileInfo
Also use explicit path variant variable names rather than mutating and overusing vague $path var.

Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-15 00:32:03 -04:00
Josh e9b0bf0eb8 refactor(View): streamline getFileInfo to use early returns
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-14 12:51:51 -04:00
Josh 0d00b17d9d docs(View): update docblock and comments in getFileInfo
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-14 12:37:26 -04:00
742 changed files with 3424 additions and 5685 deletions
+1 -1
View File
@@ -56,4 +56,4 @@ jobs:
- name: PHPUnit
uses: docker://ghcr.io/nextcloud/continuous-integration-php8.4-32bit:latest
with:
args: /bin/sh -c "composer run test -- --exclude-group PRIMARY-azure --exclude-group PRIMARY-s3 --exclude-group PRIMARY-swift --exclude-group Memcached --exclude-group Redis --exclude-group RoutingWeirdness"
args: /bin/sh -c "composer run test -- --exclude-group PRIMARY-azure,PRIMARY-s3,PRIMARY-swift,Memcached,Redis,RoutingWeirdness"
+6 -3
View File
@@ -129,10 +129,11 @@
## Rule: Map /remote* --> /remote.php* including the query string
##
## Context:
## - XXX: `QSA` seems unnecessary (no-op) here (query string is passed by default when the replacement URI doesn't contain a query string)
## - XXX: Is this even used anymore? Seems a relic from <NC12
##
RewriteRule ^remote/(.*) remote.php [L]
RewriteRule ^remote/(.*) remote.php [QSA,L]
##
## Rule: Prevent access to non-public files
@@ -147,19 +148,21 @@
## - Intentionally excludes URIs used for HTTPS certificate verifications
## - RFC 8555 / ACME HTTP Challenges (acme-challenge)
## - File-based Validations (pki-validation)
## - XXX: `QSA` seems unnecessary (no-op) here (query string is passed by default when the replacement URI doesn't contain a query string)
## - XXX: Sometimes we are using `/index.php` and other times `index.php` as our replacement URI; this may be incorrect
##
RewriteRule ^\.well-known/(?!acme-challenge|pki-validation) /index.php [L]
RewriteRule ^\.well-known/(?!acme-challenge|pki-validation) /index.php [QSA,L]
##
## Rule: Map the ocm-provider handling to our main frontend controller (/index.php)
##
## Context:
## - XXX: `QSA` seems unnecessary (no-op) here (query string is passed by default when the replacement URI doesn't contain a query string)
## - XXX: Sometimes we are using `/index.php` and other times `index.php` as our replacement URI; this may be incorrect
##
RewriteRule ^ocm-provider/?$ index.php [L]
RewriteRule ^ocm-provider/?$ index.php [QSA,L]
##
## Rule: Prevent access to more non-public files
-6
View File
@@ -13,7 +13,6 @@
.gitmodules
.idea
.jshint
.jshintrc
.l10nignore
.mailmap
.nextcloudignore
@@ -33,9 +32,7 @@ SECURITY.md
codecov.yml
cs-fixer
csfixer
custom.d.ts
cypress
cypress.config.ts
eslint.config.js
flake.lock
flake.nix
@@ -47,7 +44,6 @@ rector
stylelint.config.js
tests
tsconfig.json
vite.config.ts
vitest.config.ts
window.d.ts
@@ -60,5 +56,3 @@ window.d.ts
/config/config.php
/contribute
/data
/openapi.json
/vendor-bin
@@ -210,7 +210,6 @@ return array(
'OCA\\DAV\\ConfigLexicon' => $baseDir . '/../lib/ConfigLexicon.php',
'OCA\\DAV\\Connector\\LegacyDAVACL' => $baseDir . '/../lib/Connector/LegacyDAVACL.php',
'OCA\\DAV\\Connector\\LegacyPublicAuth' => $baseDir . '/../lib/Connector/LegacyPublicAuth.php',
'OCA\\DAV\\Connector\\Sabre\\AddExtraHeadersPlugin' => $baseDir . '/../lib/Connector/Sabre/AddExtraHeadersPlugin.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',
@@ -225,7 +225,6 @@ class ComposerStaticInitDAV
'OCA\\DAV\\ConfigLexicon' => __DIR__ . '/..' . '/../lib/ConfigLexicon.php',
'OCA\\DAV\\Connector\\LegacyDAVACL' => __DIR__ . '/..' . '/../lib/Connector/LegacyDAVACL.php',
'OCA\\DAV\\Connector\\LegacyPublicAuth' => __DIR__ . '/..' . '/../lib/Connector/LegacyPublicAuth.php',
'OCA\\DAV\\Connector\\Sabre\\AddExtraHeadersPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/AddExtraHeadersPlugin.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',
-2
View File
@@ -236,7 +236,6 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Nepodařilo se zkontrolovat velikost souboru: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Nebylo možné otevřít soubor: %1$s (%2$d) zdá se, že soubor existuje",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Nebylo možné otevřít soubor: %1$s (%2$d) zdá se, že soubor neexistuje",
"Failed to get size for : %1$s" : "Nepodařilo se získat velikost pro: %1$s",
"Encryption not ready: %1$s" : "Šifrování není připraveno: %1$s",
"Failed to open file: %1$s" : "Nepodařilo se otevřít soubor: %1$s",
"Failed to unlink: %1$s" : "Nepodařilo se zrušit propojení: %1$s",
@@ -253,7 +252,6 @@ OC.L10N.register(
"Completed on %s" : "Dokončeno %s",
"Due on %s by %s" : "Termín do %s od %s",
"Due on %s" : "Termín do %s",
"This is an example contact" : "Toto je kontakt pro ukázku",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Vítejte v Nextcloud Kalendáři!\n\nToto je událost pro ukázku prozkoumejte flexibilitu plánování pomoc Nextcloud Kalendáře upravením čeho chcete!\n\nS Nextcloud Kalendářem je možné:\n- Jednoduše vytvářet, upravovat a spravovat události.\n- Vytvářet vícero kalendářů a sdílet je s kolegy, přáteli či rodinou.\n- Zjišťovat dostupnost a zobrazovat své doby nedostupnosti ostatním.\n- Hladce napojovat na aplikace a zřízení prostřednictvím CalDAV.\n- Přizpůsobit si svůj dojem z používání: plánovat opakující se události, upravovat notifikace a ostatní nastavení.",
"Example event - open me!" : "Událost pro ukázku otevřete ji!",
"System Address Book" : "Systémový adresář kontaktů",
-2
View File
@@ -234,7 +234,6 @@
"Failed to check file size: %1$s" : "Nepodařilo se zkontrolovat velikost souboru: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Nebylo možné otevřít soubor: %1$s (%2$d) zdá se, že soubor existuje",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Nebylo možné otevřít soubor: %1$s (%2$d) zdá se, že soubor neexistuje",
"Failed to get size for : %1$s" : "Nepodařilo se získat velikost pro: %1$s",
"Encryption not ready: %1$s" : "Šifrování není připraveno: %1$s",
"Failed to open file: %1$s" : "Nepodařilo se otevřít soubor: %1$s",
"Failed to unlink: %1$s" : "Nepodařilo se zrušit propojení: %1$s",
@@ -251,7 +250,6 @@
"Completed on %s" : "Dokončeno %s",
"Due on %s by %s" : "Termín do %s od %s",
"Due on %s" : "Termín do %s",
"This is an example contact" : "Toto je kontakt pro ukázku",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Vítejte v Nextcloud Kalendáři!\n\nToto je událost pro ukázku prozkoumejte flexibilitu plánování pomoc Nextcloud Kalendáře upravením čeho chcete!\n\nS Nextcloud Kalendářem je možné:\n- Jednoduše vytvářet, upravovat a spravovat události.\n- Vytvářet vícero kalendářů a sdílet je s kolegy, přáteli či rodinou.\n- Zjišťovat dostupnost a zobrazovat své doby nedostupnosti ostatním.\n- Hladce napojovat na aplikace a zřízení prostřednictvím CalDAV.\n- Přizpůsobit si svůj dojem z používání: plánovat opakující se události, upravovat notifikace a ostatní nastavení.",
"Example event - open me!" : "Událost pro ukázku otevřete ji!",
"System Address Book" : "Systémový adresář kontaktů",
-1
View File
@@ -253,7 +253,6 @@ OC.L10N.register(
"Completed on %s" : "Completed on %s",
"Due on %s by %s" : "Due on %s by %s",
"Due on %s" : "Due on %s",
"This is an example contact" : "This is an example contact",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings.",
"Example event - open me!" : "Example event - open me!",
"System Address Book" : "System Address Book",
-1
View File
@@ -251,7 +251,6 @@
"Completed on %s" : "Completed on %s",
"Due on %s by %s" : "Due on %s by %s",
"Due on %s" : "Due on %s",
"This is an example contact" : "This is an example contact",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings.",
"Example event - open me!" : "Example event - open me!",
"System Address Book" : "System Address Book",
-2
View File
@@ -236,7 +236,6 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Impossible de vérifier la taille du fichier : %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Impossible d'ouvrir le fichier : %1$s (%2$d), le fichier semble exister",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Impossible d'ouvrir le fichier : %1$s (%2$d), le fichier ne semble pas exister",
"Failed to get size for : %1$s" : "Impossible d'obtenir la taille pour : %1$s",
"Encryption not ready: %1$s" : "Chiffrement pas prêt : %1$s",
"Failed to open file: %1$s" : "Impossible d'ouvrir le fichier : %1$s",
"Failed to unlink: %1$s" : "Impossible de supprimer le lien :%1$s",
@@ -253,7 +252,6 @@ OC.L10N.register(
"Completed on %s" : "Terminé le %s",
"Due on %s by %s" : "Echéance le %s pour %s",
"Due on %s" : "Echéance le %s",
"This is an example contact" : "Ceci est un contact pour l'exemple",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Bienvenue dans Nextcloud Calendar !\n\nCeci est un exemple d'événement. Découvrez la flexibilité de la planification avec Nextcloud Calendar en effectuant toutes les modifications que vous souhaitez !\n\nAvec Nextcloud Calendar, vous pouvez :\n- Créer, modifier et gérer des événements sans effort.\n- Créer plusieurs calendriers et les partager avec vos collègues, vos amis ou votre famille.\n- Vérifier vos disponibilités et afficher vos périodes d'indisponibilité à d'autres personnes.\n- Intégrer de manière transparente des applications et des appareils via CalDAV.\n- Personnaliser votre expérience : planifier des événements récurrents, ajuster les notifications et d'autres paramètres.",
"Example event - open me!" : "Exemple d'événement - ouvrez-moi !",
"System Address Book" : "Carnet d'adresses du système",
-2
View File
@@ -234,7 +234,6 @@
"Failed to check file size: %1$s" : "Impossible de vérifier la taille du fichier : %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Impossible d'ouvrir le fichier : %1$s (%2$d), le fichier semble exister",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Impossible d'ouvrir le fichier : %1$s (%2$d), le fichier ne semble pas exister",
"Failed to get size for : %1$s" : "Impossible d'obtenir la taille pour : %1$s",
"Encryption not ready: %1$s" : "Chiffrement pas prêt : %1$s",
"Failed to open file: %1$s" : "Impossible d'ouvrir le fichier : %1$s",
"Failed to unlink: %1$s" : "Impossible de supprimer le lien :%1$s",
@@ -251,7 +250,6 @@
"Completed on %s" : "Terminé le %s",
"Due on %s by %s" : "Echéance le %s pour %s",
"Due on %s" : "Echéance le %s",
"This is an example contact" : "Ceci est un contact pour l'exemple",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Bienvenue dans Nextcloud Calendar !\n\nCeci est un exemple d'événement. Découvrez la flexibilité de la planification avec Nextcloud Calendar en effectuant toutes les modifications que vous souhaitez !\n\nAvec Nextcloud Calendar, vous pouvez :\n- Créer, modifier et gérer des événements sans effort.\n- Créer plusieurs calendriers et les partager avec vos collègues, vos amis ou votre famille.\n- Vérifier vos disponibilités et afficher vos périodes d'indisponibilité à d'autres personnes.\n- Intégrer de manière transparente des applications et des appareils via CalDAV.\n- Personnaliser votre expérience : planifier des événements récurrents, ajuster les notifications et d'autres paramètres.",
"Example event - open me!" : "Exemple d'événement - ouvrez-moi !",
"System Address Book" : "Carnet d'adresses du système",
-1
View File
@@ -253,7 +253,6 @@ OC.L10N.register(
"Completed on %s" : "Críochnaithe ar %s",
"Due on %s by %s" : "Dlite ar %s faoi %s",
"Due on %s" : "Dlite ar %s",
"This is an example contact" : "Seo sampla teagmhála",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Fáilte go Féilire Nextcloud!\n\nSeo sampla imeachta - déan iniúchadh ar sholúbthacht na pleanála le Féilire Nextcloud trí aon eagarthóireacht is mian leat a dhéanamh!\n\nLe Féilire Nextcloud, is féidir leat:\n- Imeachtaí a chruthú, a chur in eagar agus a bhainistiú gan stró.\n- Ilfhéilirí a chruthú agus iad a roinnt le comhghleacaithe foirne, cairde nó teaghlach.\n- Infhaighteacht a sheiceáil agus do chuid amanna gnóthacha a thaispeáint do dhaoine eile.\n- Comhtháthú gan uaim le haipeanna agus gléasanna trí CalDAV.\n- Do thaithí a shaincheapadh: imeachtaí athfhillteacha a sceidealú, fógraí agus socruithe eile a choigeartú.",
"Example event - open me!" : "Imeacht shamplach - oscail mé!",
"System Address Book" : "Leabhar Seoltaí Córais",
-1
View File
@@ -251,7 +251,6 @@
"Completed on %s" : "Críochnaithe ar %s",
"Due on %s by %s" : "Dlite ar %s faoi %s",
"Due on %s" : "Dlite ar %s",
"This is an example contact" : "Seo sampla teagmhála",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Fáilte go Féilire Nextcloud!\n\nSeo sampla imeachta - déan iniúchadh ar sholúbthacht na pleanála le Féilire Nextcloud trí aon eagarthóireacht is mian leat a dhéanamh!\n\nLe Féilire Nextcloud, is féidir leat:\n- Imeachtaí a chruthú, a chur in eagar agus a bhainistiú gan stró.\n- Ilfhéilirí a chruthú agus iad a roinnt le comhghleacaithe foirne, cairde nó teaghlach.\n- Infhaighteacht a sheiceáil agus do chuid amanna gnóthacha a thaispeáint do dhaoine eile.\n- Comhtháthú gan uaim le haipeanna agus gléasanna trí CalDAV.\n- Do thaithí a shaincheapadh: imeachtaí athfhillteacha a sceidealú, fógraí agus socruithe eile a choigeartú.",
"Example event - open me!" : "Imeacht shamplach - oscail mé!",
"System Address Book" : "Leabhar Seoltaí Córais",
-2
View File
@@ -236,7 +236,6 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Neuspjela provjera veličine datoteke: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Nije moguće otvoriti datoteku: %1$s (%2$d), čini se da datoteka postoji",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Nije moguće otvoriti datoteku: %1$s (%2$d), čini se da datoteka ne postoji",
"Failed to get size for : %1$s" : "Neuspjelo dohvaćanje veličine: %1$s",
"Encryption not ready: %1$s" : "Šifriranje nije spremno: %1$s",
"Failed to open file: %1$s" : "Neuspjelo otvaranje datoteke: %1$s",
"Failed to unlink: %1$s" : "Neuspjelo uklanjanje veze: %1$s",
@@ -253,7 +252,6 @@ OC.L10N.register(
"Completed on %s" : "Završeno na %s",
"Due on %s by %s" : "%s treba završiti do %s",
"Due on %s" : "Treba završiti do %s",
"This is an example contact" : "Ovo je primjer kontakta",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Dobrodošli u Nextcloud Kalendar!\n\nOvo je primjer događaja istražite fleksibilnost planiranja s Nextcloud Kalendarom tako da napravite bilo kakve izmjene koje želite!\n\nUz Nextcloud Kalendar možete:\n- Jednostavno stvarati, uređivati i upravljati događajima..\n- Stvarati više kalendara i dijeliti ih s kolegama, prijateljima ili obitelji.\n- Provjeravati dostupnost i drugima prikazivati svoja zauzeta razdoblja.\n- Neprimjetno se integrirati s aplikacijama i uređajima putem CalDAV-a.\n- Prilagoditi svoje iskustvo: zakazivati ponavljajuće događaje, prilagođavati obavijesti i druge postavke.",
"Example event - open me!" : "Primjer događaja otvori me!",
"System Address Book" : "Adresar sustava",
-2
View File
@@ -234,7 +234,6 @@
"Failed to check file size: %1$s" : "Neuspjela provjera veličine datoteke: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Nije moguće otvoriti datoteku: %1$s (%2$d), čini se da datoteka postoji",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Nije moguće otvoriti datoteku: %1$s (%2$d), čini se da datoteka ne postoji",
"Failed to get size for : %1$s" : "Neuspjelo dohvaćanje veličine: %1$s",
"Encryption not ready: %1$s" : "Šifriranje nije spremno: %1$s",
"Failed to open file: %1$s" : "Neuspjelo otvaranje datoteke: %1$s",
"Failed to unlink: %1$s" : "Neuspjelo uklanjanje veze: %1$s",
@@ -251,7 +250,6 @@
"Completed on %s" : "Završeno na %s",
"Due on %s by %s" : "%s treba završiti do %s",
"Due on %s" : "Treba završiti do %s",
"This is an example contact" : "Ovo je primjer kontakta",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Dobrodošli u Nextcloud Kalendar!\n\nOvo je primjer događaja istražite fleksibilnost planiranja s Nextcloud Kalendarom tako da napravite bilo kakve izmjene koje želite!\n\nUz Nextcloud Kalendar možete:\n- Jednostavno stvarati, uređivati i upravljati događajima..\n- Stvarati više kalendara i dijeliti ih s kolegama, prijateljima ili obitelji.\n- Provjeravati dostupnost i drugima prikazivati svoja zauzeta razdoblja.\n- Neprimjetno se integrirati s aplikacijama i uređajima putem CalDAV-a.\n- Prilagoditi svoje iskustvo: zakazivati ponavljajuće događaje, prilagođavati obavijesti i druge postavke.",
"Example event - open me!" : "Primjer događaja otvori me!",
"System Address Book" : "Adresar sustava",
-1
View File
@@ -253,7 +253,6 @@ OC.L10N.register(
"Completed on %s" : "%s tarihinde tamamlandı",
"Due on %s by %s" : "%s tarihine kadar %s tarafından",
"Due on %s" : "%s tarihine kadar",
"This is an example contact" : "Bu bir kişi örneğidir",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Nextcloud Takvim uygulamasına hoş geldiniz!\n\nBu bir örnek etkinliktir. İstediğiniz düzenlemeleri yaparak Nextcloud Takvim ile planlamanın esnekliğini keşfedin!\n\nNextcloud Takvim ile şunları yapabilirsiniz:\n- Etkinlikleri kolayca oluşturabilir, düzenleyebilir ve yönetebilirsiniz.\n- Birden fazla takvim oluşturabilir ve bunları takım arkadaşlarınız, arkadaşlarınız veya ailenizle paylaşabilirsiniz.\n- Uygunluğunuzu kontrol edebilir ve yoğun zamanlarınızı başkalarına gösterebilirsiniz.\n- CalDAV aracılığıyla uygulamaları ve aygıtları sorunsuz bir şekilde bütünleştirebilirsiniz.\n- Deneyiminizi özelleştirebilirsiniz: Yinelenen etkinlikler planlayabilir, bildirimleri ve diğer ayarları ayarlayabilirsiniz.",
"Example event - open me!" : "Örnek etkinlik. Beni aç!",
"System Address Book" : "Sistem adres defteri",
-1
View File
@@ -251,7 +251,6 @@
"Completed on %s" : "%s tarihinde tamamlandı",
"Due on %s by %s" : "%s tarihine kadar %s tarafından",
"Due on %s" : "%s tarihine kadar",
"This is an example contact" : "Bu bir kişi örneğidir",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Nextcloud Takvim uygulamasına hoş geldiniz!\n\nBu bir örnek etkinliktir. İstediğiniz düzenlemeleri yaparak Nextcloud Takvim ile planlamanın esnekliğini keşfedin!\n\nNextcloud Takvim ile şunları yapabilirsiniz:\n- Etkinlikleri kolayca oluşturabilir, düzenleyebilir ve yönetebilirsiniz.\n- Birden fazla takvim oluşturabilir ve bunları takım arkadaşlarınız, arkadaşlarınız veya ailenizle paylaşabilirsiniz.\n- Uygunluğunuzu kontrol edebilir ve yoğun zamanlarınızı başkalarına gösterebilirsiniz.\n- CalDAV aracılığıyla uygulamaları ve aygıtları sorunsuz bir şekilde bütünleştirebilirsiniz.\n- Deneyiminizi özelleştirebilirsiniz: Yinelenen etkinlikler planlayabilir, bildirimleri ve diğer ayarları ayarlayabilirsiniz.",
"Example event - open me!" : "Örnek etkinlik. Beni aç!",
"System Address Book" : "Sistem adres defteri",
-2
View File
@@ -236,7 +236,6 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "检查文件大小失败:%1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "无法打开文件:%1$s%2$d),文件似乎不存在",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "无法打开文件:%1$s%2$d),文件似乎不存在",
"Failed to get size for : %1$s" : "无法获取以下项目的大小:%1$s",
"Encryption not ready: %1$s" : "加密不可用:%1$s",
"Failed to open file: %1$s" : "打开文件失败:%1$s",
"Failed to unlink: %1$s" : "解除链接失败:%1$s",
@@ -253,7 +252,6 @@ OC.L10N.register(
"Completed on %s" : "已完成 %s",
"Due on %s by %s" : "到期于 %s,在 %s 之前",
"Due on %s" : "到期于 %s",
"This is an example contact" : "这是一个示例联系人",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "欢迎使用 Nextcloud 日历!\n\n这是一个示例事件——探索使用 Nextcloud 日历进行规划的灵活性,进行任何您想要的编辑!\n\n使用 Nextcloud 日历,您可以:\n- 轻松创建、编辑和管理事件。\n- 创建多个日历并与队友、朋友或家人共享。\n- 查看空闲时间并向他人显示您的忙碌时间。\n- 通过 CalDAV 与应用和设备无缝集成。\n- 自定义您的体验:安排重复事件、调整通知和其他设置。",
"Example event - open me!" : "示例事件——打开我!",
"System Address Book" : "系统通讯录",
-2
View File
@@ -234,7 +234,6 @@
"Failed to check file size: %1$s" : "检查文件大小失败:%1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "无法打开文件:%1$s%2$d),文件似乎不存在",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "无法打开文件:%1$s%2$d),文件似乎不存在",
"Failed to get size for : %1$s" : "无法获取以下项目的大小:%1$s",
"Encryption not ready: %1$s" : "加密不可用:%1$s",
"Failed to open file: %1$s" : "打开文件失败:%1$s",
"Failed to unlink: %1$s" : "解除链接失败:%1$s",
@@ -251,7 +250,6 @@
"Completed on %s" : "已完成 %s",
"Due on %s by %s" : "到期于 %s,在 %s 之前",
"Due on %s" : "到期于 %s",
"This is an example contact" : "这是一个示例联系人",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "欢迎使用 Nextcloud 日历!\n\n这是一个示例事件——探索使用 Nextcloud 日历进行规划的灵活性,进行任何您想要的编辑!\n\n使用 Nextcloud 日历,您可以:\n- 轻松创建、编辑和管理事件。\n- 创建多个日历并与队友、朋友或家人共享。\n- 查看空闲时间并向他人显示您的忙碌时间。\n- 通过 CalDAV 与应用和设备无缝集成。\n- 自定义您的体验:安排重复事件、调整通知和其他设置。",
"Example event - open me!" : "示例事件——打开我!",
"System Address Book" : "系统通讯录",
-1
View File
@@ -253,7 +253,6 @@ OC.L10N.register(
"Completed on %s" : "完成於 %s",
"Due on %s by %s" : "完成日期為 %s %s",
"Due on %s" : "完成日期 %s",
"This is an example contact" : "此為示例聯絡人",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "歡迎使用 Nextcloud 日曆!\n\n這是範例事件 - 使用 Nextcloud 日曆進行任何編輯,探索規劃的彈性!\n\n使用 Nextcloud 日曆,您可以:\n- 毫不費力地建立、編輯與管理活動。\n- 建立多個日曆,並與同事、朋友或家人分享。\n- 檢查可得性,並向他人顯示您的忙碌時間。\n- 透過 CalDAV 與應用程式與裝置無縫整合。\n- 自訂您的體驗:排定定期活動、調整通知與其他設定。",
"Example event - open me!" : "範例活動 - 打開我!",
"System Address Book" : "系統通訊錄",
-1
View File
@@ -251,7 +251,6 @@
"Completed on %s" : "完成於 %s",
"Due on %s by %s" : "完成日期為 %s %s",
"Due on %s" : "完成日期 %s",
"This is an example contact" : "此為示例聯絡人",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "歡迎使用 Nextcloud 日曆!\n\n這是範例事件 - 使用 Nextcloud 日曆進行任何編輯,探索規劃的彈性!\n\n使用 Nextcloud 日曆,您可以:\n- 毫不費力地建立、編輯與管理活動。\n- 建立多個日曆,並與同事、朋友或家人分享。\n- 檢查可得性,並向他人顯示您的忙碌時間。\n- 透過 CalDAV 與應用程式與裝置無縫整合。\n- 自訂您的體驗:排定定期活動、調整通知與其他設定。",
"Example event - open me!" : "範例活動 - 打開我!",
"System Address Book" : "系統通訊錄",
-4
View File
@@ -2062,10 +2062,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$outerQuery->andWhere($outerQuery->expr()->eq('uid', $outerQuery->createNamedParameter($options['uid'])));
}
if (isset($options['uri'])) {
$outerQuery->andWhere($outerQuery->expr()->eq('uri', $outerQuery->createNamedParameter($options['uri'])));
}
if (!empty($options['types'])) {
$or = [];
foreach ($options['types'] as $type) {
+2 -9
View File
@@ -16,7 +16,6 @@ use OCP\Calendar\CalendarExportOptions;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\Calendar\ICalendarExport;
use OCP\Calendar\ICalendarIsEnabled;
use OCP\Calendar\ICalendarIsPublic;
use OCP\Calendar\ICalendarIsShared;
use OCP\Calendar\ICalendarIsWritable;
use OCP\Calendar\ICreateFromString;
@@ -33,7 +32,7 @@ use Sabre\VObject\Reader;
use function Sabre\Uri\split as uriSplit;
class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIsWritable, ICalendarIsShared, ICalendarExport, ICalendarIsEnabled, ICalendarIsPublic {
class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIsWritable, ICalendarIsShared, ICalendarExport, ICalendarIsEnabled {
public function __construct(
private Calendar $calendar,
/** @var array<string, mixed> */
@@ -169,13 +168,6 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIs
return $this->calendar->isShared();
}
/**
* @since 33.0.1, 32.0.7, 31.0.14.1, 30.0.17.8
*/
public function getPublicToken(): ?string {
return $this->calendar->getPublishStatus() ?: null;
}
/**
* @throws CalendarException
*/
@@ -344,4 +336,5 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIs
}
}
}
}
@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* Adds the "OC-OwnerId" and "OC-Permissions" after PUT requests so that
* clients don't need to do a propfind after uploading a file to decide what
* to display.
*/
class AddExtraHeadersPlugin extends \Sabre\DAV\ServerPlugin {
private ?Server $server = null;
public function __construct(
private LoggerInterface $logger,
private bool $isPublic = false,
) {
}
public function initialize(Server $server): void {
$this->server = $server;
$server->on('afterMethod:PUT', $this->afterPut(...));
}
private function afterPut(RequestInterface $request, ResponseInterface $response): void {
if ($this->server === null) {
return;
}
$node = null;
try {
$node = $this->server->tree->getNodeForPath($request->getPath());
} catch (NotFound) {
$this->logger->error("Cannot set extra headers for non-existing file '{$request->getPath()}'");
return;
}
if (!$node instanceof Node) {
$nodeType = get_debug_type($node);
$this->logger->error("Cannot set extra headers for node of type {$nodeType} for file '{$request->getPath()}'");
return;
}
if (!$this->isPublic) {
$ownerId = $node->getOwner()?->getUID();
if ($ownerId !== null) {
$response->setHeader('X-NC-OwnerId', $ownerId);
}
}
$permissions = $this->isPublic ? $node->getPublicDavPermissions()
: $node->getDavPermissions();
$response->setHeader('X-NC-Permissions', $permissions);
}
}
@@ -49,7 +49,7 @@ class BlockLegacyClientPlugin extends ServerPlugin {
return;
}
$minimumSupportedDesktopVersion = $this->config->getSystemValueString('minimum.supported.desktop.version', '3.1.81');
$minimumSupportedDesktopVersion = $this->config->getSystemValueString('minimum.supported.desktop.version', '3.1.50');
$maximumSupportedDesktopVersion = $this->config->getSystemValueString('maximum.supported.desktop.version', '99.99.99');
// Check if the client is a desktop client
+6 -2
View File
@@ -40,7 +40,6 @@ use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class FilesPlugin extends ServerPlugin {
// namespace
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
@@ -312,7 +311,12 @@ class FilesPlugin extends ServerPlugin {
});
$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
return $this->isPublic ? $node->getPublicDavPermissions() : $node->getDavPermissions();
$perms = $node->getDavPermissions();
if ($this->isPublic) {
// remove mount information
$perms = str_replace(['S', 'M'], '', $perms);
}
return $perms;
});
$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
-7
View File
@@ -323,13 +323,6 @@ abstract class Node implements INode {
return DavUtil::getDavPermissions($this->info);
}
/**
* Returns the DAV Permissions with share and mount infromation stripped.
*/
public function getPublicDavPermissions(): string {
return str_replace(['S', 'M'], '', $this->getDavPermissions());
}
public function getOwner(): ?IUser {
return $this->info->getOwner();
}
+2 -2
View File
@@ -258,8 +258,8 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
if ($length > $freeSpace) {
$msg = $isDir
? "Insufficient space in $normalizedPath. Cannot create directory"
: "Insufficient space in $normalizedPath";
? "Insufficient space in $normalizedPath. $freeSpace available. Cannot create directory"
: "Insufficient space in $normalizedPath, $length required, $freeSpace available";
throw new InsufficientStorage($msg);
}
@@ -209,7 +209,6 @@ class ServerFactory {
);
}
$server->addPlugin(new CopyEtagHeaderPlugin());
$server->addPlugin(new AddExtraHeadersPlugin($this->logger, $isPublicShare));
// Load dav plugins from apps
$event = new SabrePluginEvent($server);
-2
View File
@@ -27,7 +27,6 @@ use OCA\DAV\CardDAV\PhotoCache;
use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin;
use OCA\DAV\CardDAV\Validation\CardDavValidatePlugin;
use OCA\DAV\Comments\CommentsPlugin;
use OCA\DAV\Connector\Sabre\AddExtraHeadersPlugin;
use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
use OCA\DAV\Connector\Sabre\AppleQuirksPlugin;
use OCA\DAV\Connector\Sabre\Auth;
@@ -386,7 +385,6 @@ class Server {
)
);
}
$this->server->addPlugin(new AddExtraHeadersPlugin($logger, false));
$this->server->addPlugin(new EnablePlugin(
\OCP\Server::get(IConfig::class),
\OCP\Server::get(BirthdayService::class),
@@ -238,14 +238,14 @@ class CalendarMigratorTest extends TestCase {
$exportedData = null;
$exportDestination->method('addFileContents')
->willReturnCallback(function (string $path, string $content) use (&$exportedCalendarsJson): void {
->willReturnCallback(function (string $path, string $content) use (&$exportedCalendarsJson) {
if ($path === 'dav/calendars/calendars.json') {
$exportedCalendarsJson = json_decode($content, true);
}
});
$exportDestination->method('addFileAsStream')
->willReturnCallback(function (string $path, $stream) use (&$exportedData): void {
->willReturnCallback(function (string $path, $stream) use (&$exportedData) {
if (str_ends_with($path, '.data')) {
$exportedData = stream_get_contents($stream);
}
@@ -296,11 +296,11 @@ class CalendarMigratorTest extends TestCase {
$exportDestination = $this->createMock(IExportDestination::class);
$exportDestination->method('addFileContents')
->willReturnCallback(function (string $path, string $content) use (&$exportedFiles): void {
->willReturnCallback(function (string $path, string $content) use (&$exportedFiles) {
$exportedFiles[$path] = $content;
});
$exportDestination->method('addFileAsStream')
->willReturnCallback(function (string $path, $stream) use (&$exportedFiles): void {
->willReturnCallback(function (string $path, $stream) use (&$exportedFiles) {
$exportedFiles[$path] = stream_get_contents($stream);
});
@@ -469,7 +469,7 @@ class CalendarMigratorTest extends TestCase {
$exportedSubscriptionsJson = null;
$exportDestination->method('addFileContents')
->willReturnCallback(function (string $path, string $content) use (&$exportedSubscriptionsJson): void {
->willReturnCallback(function (string $path, string $content) use (&$exportedSubscriptionsJson) {
if ($path === 'dav/calendars/subscriptions.json') {
$exportedSubscriptionsJson = json_decode($content, true);
}
@@ -578,7 +578,7 @@ class CalendarMigratorTest extends TestCase {
$exportDestination = $this->createMock(IExportDestination::class);
$exportDestination->method('addFileContents')
->willReturnCallback(function (string $path, string $content) use (&$exportedFiles): void {
->willReturnCallback(function (string $path, string $content) use (&$exportedFiles) {
$exportedFiles[$path] = $content;
});
@@ -1840,72 +1840,6 @@ EOD;
$this->assertEquals('Missing DTSTART 2', $results[3]['objects'][0]['SUMMARY'][0]);
}
public function testSearchByUri(): void {
$calendarId = $this->createTestCalendar();
$uris = [];
$calData = [];
$uris[] = static::getUniqueID('calobj');
$calData[] = <<<'EOD'
BEGIN:VCALENDAR
VERSION:2.0
PRODID:Nextcloud Calendar
BEGIN:VEVENT
CREATED;VALUE=DATE-TIME:20260323T093039Z
UID:search-by-uri-test1
LAST-MODIFIED;VALUE=DATE-TIME:20260323T093039Z
DTSTAMP;VALUE=DATE-TIME:20260323T093039Z
SUMMARY:First Test Event
DTSTART;VALUE=DATE-TIME:20260323T093039Z
DTEND;VALUE=DATE-TIME:20260323T093039Z
CLASS:PUBLIC
END:VEVENT
END:VCALENDAR
EOD;
$uris[] = static::getUniqueID('calobj');
$calData[] = <<<'EOD'
BEGIN:VCALENDAR
VERSION:2.0
PRODID:Nextcloud Calendar
BEGIN:VEVENT
CREATED;VALUE=DATE-TIME:20260323T093039Z
UID:search-by-uri-test2
LAST-MODIFIED;VALUE=DATE-TIME:20260323T093039Z
DTSTAMP;VALUE=DATE-TIME:20260323T093039Z
SUMMARY:Second Test Event
DTSTART;VALUE=DATE-TIME:20260323T093039Z
DTEND;VALUE=DATE-TIME:20260323T093039Z
CLASS:PUBLIC
END:VEVENT
END:VCALENDAR
EOD;
foreach ($uris as $i => $uri) {
$this->backend->createCalendarObject($calendarId, $uri, $calData[$i]);
}
$calendarInfo = [
'id' => $calendarId,
'principaluri' => 'user1',
'{http://owncloud.org/ns}owner-principal' => 'user1',
];
// Searching by first event's URI returns this event
$results = $this->backend->search($calendarInfo, '', [], ['uri' => $uris[0]], null, null);
$this->assertCount(1, $results);
$this->assertEquals($uris[0], $results[0]['uri']);
// Searching by second event's URI returns this event
$results = $this->backend->search($calendarInfo, '', [], ['uri' => $uris[1]], null, null);
$this->assertCount(1, $results);
$this->assertEquals($uris[1], $results[0]['uri']);
// Searching by a non-existent URI returns nothing
$result = $this->backend->search($calendarInfo, '', [], ['uri' => 'nonexistant.ical'], null, null);
$this->assertCount(0, $result);
}
public function testUnshare(): void {
$principalGroup = 'principal:' . self::UNIT_TEST_GROUP;
$principalUser = 'principal:' . self::UNIT_TEST_USER;
@@ -33,7 +33,7 @@ class CalendarImplTest extends \Test\TestCase {
$this->backend = $this->createMock(CalDavBackend::class);
$this->calendar = $this->createMock(Calendar::class);
$this->calendarInfo = [
'id' => 123,
'id' => 'fancy_id_123',
'{DAV:}displayname' => 'user readable name 123',
'{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
'uri' => '/this/is/a/uri',
@@ -62,7 +62,7 @@ class CalendarImplTest extends \Test\TestCase {
public function testGetKey(): void {
$this->assertEquals($this->calendarImpl->getKey(), '123');
$this->assertEquals($this->calendarImpl->getKey(), 'fancy_id_123');
}
public function testGetDisplayname(): void {
@@ -73,18 +73,6 @@ class CalendarImplTest extends \Test\TestCase {
$this->assertEquals($this->calendarImpl->getDisplayColor(), '#AABBCC');
}
public function testGetPublicToken(): void {
$publicToken = $this->calendar->setPublishStatus(true);
$this->assertEquals($this->calendarImpl->getPublicToken(), $publicToken);
}
public function testGetPublicTokenWithPrivateCalendar(): void {
$this->calendar->setPublishStatus(false);
$this->assertNull($this->calendarImpl->getPublicToken());
}
public function testSearch(): void {
$this->backend->expects($this->once())
->method('search')
@@ -278,4 +266,5 @@ class CalendarImplTest extends \Test\TestCase {
$calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
}
}
@@ -1,130 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace unit\Connector\Sabre;
use LogicException;
use OCA\DAV\Connector\Sabre\AddExtraHeadersPlugin;
use OCA\DAV\Connector\Sabre\Node;
use OCA\DAV\Connector\Sabre\Server;
use OCP\IUser;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Tree;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Test\TestCase;
class AddExtraHeadersPluginTest extends TestCase {
private AddExtraHeadersPlugin $plugin;
private Server&MockObject $server;
private LoggerInterface&MockObject $logger;
private RequestInterface&MockObject $request;
private ResponseInterface&MockObject $response;
private Tree&MockObject $tree;
public static function afterPutData(): array {
return [
'owner and permissions present' => [
'user', true, 'PERMISSIONS', true, 2
],
'permissions only' => [
null, false, 'PERMISSIONS', true, 1
],
];
}
public function testAfterPutNotFoundException(): void {
$afterPut = null;
$this->server->expects($this->once())
->method('on')
->willReturnCallback(
function ($method, $callback) use (&$afterPut): void {
$this->assertSame('afterMethod:PUT', $method);
$afterPut = $callback;
});
$this->plugin->initialize($this->server);
$node = $this->createMock(Node::class);
$this->tree->expects($this->once())->method('getNodeForPath')
->willThrowException(new NotFound());
$this->logger->expects($this->once())->method('error');
$afterPut($this->request, $this->response);
}
#[DataProvider('afterPutData')]
public function testAfterPut(?string $ownerId, bool $expectOwnerIdHeader,
?string $permissions, bool $expectPermissionsHeader,
int $expectedInvocations): void {
$afterPut = null;
$this->server->expects($this->once())
->method('on')
->willReturnCallback(
function ($method, $callback) use (&$afterPut): void {
$this->assertSame('afterMethod:PUT', $method);
$afterPut = $callback;
});
$this->plugin->initialize($this->server);
$node = $this->createMock(Node::class);
$this->tree->expects($this->once())->method('getNodeForPath')
->willReturn($node);
$user = $this->createMock(IUser::class);
$node->expects($this->once())->method('getOwner')->willReturn($user);
$user->expects($this->once())->method('getUID')->willReturn($ownerId);
$node->expects($this->once())->method('getDavPermissions')->willReturn($permissions);
$matcher = $this->exactly($expectedInvocations);
$this->response->expects($matcher)->method('setHeader')
->willReturnCallback(function ($name, $value) use (
$expectedInvocations,
$expectPermissionsHeader,
$expectOwnerIdHeader,
$matcher,
$ownerId, $permissions): void {
$invocationNumber = $matcher->numberOfInvocations();
if ($invocationNumber === 0) {
throw new LogicException('No invocations were expected');
}
if (($expectOwnerIdHeader && $expectedInvocations === 1)
|| ($expectedInvocations
=== 2 && $invocationNumber === 1)) {
$this->assertEquals('X-NC-OwnerId', $name);
$this->assertEquals($ownerId, $value);
}
if (($expectPermissionsHeader && $expectedInvocations === 1)
|| ($expectedInvocations
=== 2 && $invocationNumber === 2)) {
$this->assertEquals('X-NC-Permissions', $name);
$this->assertEquals($permissions, $value);
}
});
$afterPut($this->request, $this->response);
}
protected function setUp(): void {
parent::setUp();
$this->server = $this->createMock(Server::class);
$this->tree = $this->createMock(Tree::class);
$this->server->tree = $this->tree;
$this->logger = $this->createMock(LoggerInterface::class);
$this->plugin = new AddExtraHeadersPlugin($this->logger, false);
$this->request = $this->createMock(RequestInterface::class);
$this->response = $this->createMock(ResponseInterface::class);
}
}
@@ -332,12 +332,9 @@ class FilesPluginTest extends TestCase {
/** @var File&MockObject $node */
$node = $this->createTestNode(File::class);
$node->expects($this->once())
->method('getPublicDavPermissions')
->willReturn('DWCKR');
$node->expects($this->never())
->method('getDavPermissions');
$node->expects($this->any())
->method('getDavPermissions')
->willReturn('DWCKMSR');
$this->plugin->handleGetProperties(
$propFind,
@@ -18,6 +18,7 @@ use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Constants;
use OCP\Federation\ICloudIdManager;
use OCP\HintException;
use OCP\Http\Client\IClientService;
@@ -107,9 +108,9 @@ class MountPublicLinkController extends Controller {
return $response;
}
if (!$share->canDownload()) {
if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) {
$response = new JSONResponse(
['message' => 'Mounting download restricted share is not allowed'],
['message' => 'Mounting file drop not supported'],
Http::STATUS_BAD_REQUEST
);
$response->throttle();
-8
View File
@@ -15,14 +15,6 @@ OC.L10N.register(
"Add trusted server" : "Ajouter un serveur de confiance",
"Server url" : "URL du serveur",
"Add" : "Ajouter",
"Server ok" : "Serveur Ok",
"User list was exchanged at least once successfully with the remote server." : "La liste des utilisateurs a été échangée au moins une fois avec succès avec le serveur distant.",
"Server pending" : "Serveur en attente",
"Waiting for shared secret or initial user list exchange." : "En attente du partage du secret ou de l'échange initial de la liste des utilisateurs.",
"Server access revoked" : "Accès du serveur révoqué",
"Server failure" : "Échec du serveur",
"Connection to the remote server failed or the remote server is misconfigured." : "La connexion avec le serveur distant a échoué ou le serveur distant est mal configuré.",
"Failed to delete trusted server. Please try again later." : "Échec de suppression du serveur de confiance. Veuillez réessayer plus tard.",
"Delete" : "Supprimer",
"Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing. It is not necessary to add a server as trusted server in order to create a federated share." : "Une fédération vous permet de vous connecter avec d'autres serveurs de confiance pour échanger la liste des comptes. Par exemple, ce sera utilisé pour auto-compléter les comptes externes lors du partage fédéré. Il n'est pas nécessaire d'ajouter un serveur comme serveur de confiance afin de créer un partage fédéré.",
"Each server must validate the other. This process may require a few cron cycles." : "Chaque serveur doit valider l'autre. Ce processus peut prendre plusieurs cycles de tâches planifiées.",
-8
View File
@@ -13,14 +13,6 @@
"Add trusted server" : "Ajouter un serveur de confiance",
"Server url" : "URL du serveur",
"Add" : "Ajouter",
"Server ok" : "Serveur Ok",
"User list was exchanged at least once successfully with the remote server." : "La liste des utilisateurs a été échangée au moins une fois avec succès avec le serveur distant.",
"Server pending" : "Serveur en attente",
"Waiting for shared secret or initial user list exchange." : "En attente du partage du secret ou de l'échange initial de la liste des utilisateurs.",
"Server access revoked" : "Accès du serveur révoqué",
"Server failure" : "Échec du serveur",
"Connection to the remote server failed or the remote server is misconfigured." : "La connexion avec le serveur distant a échoué ou le serveur distant est mal configuré.",
"Failed to delete trusted server. Please try again later." : "Échec de suppression du serveur de confiance. Veuillez réessayer plus tard.",
"Delete" : "Supprimer",
"Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing. It is not necessary to add a server as trusted server in order to create a federated share." : "Une fédération vous permet de vous connecter avec d'autres serveurs de confiance pour échanger la liste des comptes. Par exemple, ce sera utilisé pour auto-compléter les comptes externes lors du partage fédéré. Il n'est pas nécessaire d'ajouter un serveur comme serveur de confiance afin de créer un partage fédéré.",
"Each server must validate the other. This process may require a few cron cycles." : "Chaque serveur doit valider l'autre. Ce processus peut prendre plusieurs cycles de tâches planifiées.",
-1
View File
@@ -95,7 +95,6 @@ OC.L10N.register(
"Another entry with the same name already exists." : "Už existuje jiná položka se stejným názvem.",
"Invalid filename." : "Neplatný název souboru.",
"Rename file" : "Přejmenovat soubor",
"Recently created" : "Nedávno vytvořeno",
"Folder" : "Složka",
"Unknown file type" : "Neznámý typ souboru",
"{ext} image" : "{ext}obrázek",
-1
View File
@@ -93,7 +93,6 @@
"Another entry with the same name already exists." : "Už existuje jiná položka se stejným názvem.",
"Invalid filename." : "Neplatný název souboru.",
"Rename file" : "Přejmenovat soubor",
"Recently created" : "Nedávno vytvořeno",
"Folder" : "Složka",
"Unknown file type" : "Neznámý typ souboru",
"{ext} image" : "{ext}obrázek",
-1
View File
@@ -95,7 +95,6 @@ OC.L10N.register(
"Another entry with the same name already exists." : "Une autre entrée avec le même nom existe déjà.",
"Invalid filename." : "Nom de fichier invalide.",
"Rename file" : "Renommer le fichier",
"Recently created" : "Créé récemment",
"Folder" : "Dossier",
"Unknown file type" : "Type de fichier inconnu",
"{ext} image" : "{ext} image",
-1
View File
@@ -93,7 +93,6 @@
"Another entry with the same name already exists." : "Une autre entrée avec le même nom existe déjà.",
"Invalid filename." : "Nom de fichier invalide.",
"Rename file" : "Renommer le fichier",
"Recently created" : "Créé récemment",
"Folder" : "Dossier",
"Unknown file type" : "Type de fichier inconnu",
"{ext} image" : "{ext} image",
-1
View File
@@ -95,7 +95,6 @@ OC.L10N.register(
"Another entry with the same name already exists." : "Već postoji drugi upis s istim nazivom.",
"Invalid filename." : "Nevažeći naziv datoteke.",
"Rename file" : "Preimenuj datoteku",
"Recently created" : "Nedavno stvoreno",
"Folder" : "Mapa",
"Unknown file type" : "Nepoznata vrsta datoteke",
"{ext} image" : "{ext} slika",
-1
View File
@@ -93,7 +93,6 @@
"Another entry with the same name already exists." : "Već postoji drugi upis s istim nazivom.",
"Invalid filename." : "Nevažeći naziv datoteke.",
"Rename file" : "Preimenuj datoteku",
"Recently created" : "Nedavno stvoreno",
"Folder" : "Mapa",
"Unknown file type" : "Nepoznata vrsta datoteke",
"{ext} image" : "{ext} slika",
+16 -17
View File
@@ -1,25 +1,24 @@
OC.L10N.register(
"files",
{
"Added to favorites" : "დაემატა რჩეულებში",
"Removed from favorites" : "ამოიშალა რჩეულებიდან",
"You added {file} to your favorites" : "თქვენ დაამატეთ {file}-ი რჩეულებში",
"You removed {file} from your favorites" : "თქვენ ამოშალეთ {file}-ი თქვენი რჩეული ფაილებიდან",
"Added to favorites" : "Added to favorites",
"Removed from favorites" : "Removed from favorites",
"You added {file} to your favorites" : "You added {file} to your favorites",
"You removed {file} from your favorites" : "You removed {file} from your favorites",
"Favorites" : "ფავორიტები",
"File changes" : "ფაილის ცვლილებები",
"Created by {user}" : "შექმნა მომხმარებელმა {user}",
"Changed by {user}" : "შეცვალა მომხმარებელმა {user}",
"Deleted by {user}" : "წაშალა მომხმარებელმა {user}",
"Restored by {user}" : "აღადგინა მომხმარებელმა {user}",
"Renamed by {user}" : "სახელი გადაარქვა მომხმარებელმა {user}",
"Moved by {user}" : "გადაიტანა მომხმარებელმა {user}",
"\"remote account\"" : "დისნტანციური ანგარიში",
"You created {file}" : "თქვენ შექმენით {file}",
"You created an encrypted file in {file}" : "შექმენით დაშიფრული ფაილი {file}-ში",
"{user} created {file}" : "{user} მომხმარებელმა შექმნა {file}",
"{user} created an encrypted file in {file}" : "{user} მომხმარებელმა შექმნა დაშიფრული ფაილი {file}-ში",
"{file} was created in a public folder" : "{file} შეიქმნა საზოგადო დირექტორიაში",
"You changed {file}" : "თქვენ შეცვალეთ {file}",
"Created by {user}" : "Created by {user}",
"Changed by {user}" : "Changed by {user}",
"Deleted by {user}" : "Deleted by {user}",
"Restored by {user}" : "Restored by {user}",
"Renamed by {user}" : "Renamed by {user}",
"Moved by {user}" : "Moved by {user}",
"You created {file}" : "You created {file}",
"You created an encrypted file in {file}" : "You created an encrypted file in {file}",
"{user} created {file}" : "{user} created {file}",
"{user} created an encrypted file in {file}" : "{user} created an encrypted file in {file}",
"{file} was created in a public folder" : "{file} was created in a public folder",
"You changed {file}" : "You changed {file}",
"You changed an encrypted file in {file}" : "You changed an encrypted file in {file}",
"{user} changed {file}" : "{user} changed {file}",
"{user} changed an encrypted file in {file}" : "{user} changed an encrypted file in {file}",
+16 -17
View File
@@ -1,23 +1,22 @@
{ "translations": {
"Added to favorites" : "დაემატა რჩეულებში",
"Removed from favorites" : "ამოიშალა რჩეულებიდან",
"You added {file} to your favorites" : "თქვენ დაამატეთ {file}-ი რჩეულებში",
"You removed {file} from your favorites" : "თქვენ ამოშალეთ {file}-ი თქვენი რჩეული ფაილებიდან",
"Added to favorites" : "Added to favorites",
"Removed from favorites" : "Removed from favorites",
"You added {file} to your favorites" : "You added {file} to your favorites",
"You removed {file} from your favorites" : "You removed {file} from your favorites",
"Favorites" : "ფავორიტები",
"File changes" : "ფაილის ცვლილებები",
"Created by {user}" : "შექმნა მომხმარებელმა {user}",
"Changed by {user}" : "შეცვალა მომხმარებელმა {user}",
"Deleted by {user}" : "წაშალა მომხმარებელმა {user}",
"Restored by {user}" : "აღადგინა მომხმარებელმა {user}",
"Renamed by {user}" : "სახელი გადაარქვა მომხმარებელმა {user}",
"Moved by {user}" : "გადაიტანა მომხმარებელმა {user}",
"\"remote account\"" : "დისნტანციური ანგარიში",
"You created {file}" : "თქვენ შექმენით {file}",
"You created an encrypted file in {file}" : "შექმენით დაშიფრული ფაილი {file}-ში",
"{user} created {file}" : "{user} მომხმარებელმა შექმნა {file}",
"{user} created an encrypted file in {file}" : "{user} მომხმარებელმა შექმნა დაშიფრული ფაილი {file}-ში",
"{file} was created in a public folder" : "{file} შეიქმნა საზოგადო დირექტორიაში",
"You changed {file}" : "თქვენ შეცვალეთ {file}",
"Created by {user}" : "Created by {user}",
"Changed by {user}" : "Changed by {user}",
"Deleted by {user}" : "Deleted by {user}",
"Restored by {user}" : "Restored by {user}",
"Renamed by {user}" : "Renamed by {user}",
"Moved by {user}" : "Moved by {user}",
"You created {file}" : "You created {file}",
"You created an encrypted file in {file}" : "You created an encrypted file in {file}",
"{user} created {file}" : "{user} created {file}",
"{user} created an encrypted file in {file}" : "{user} created an encrypted file in {file}",
"{file} was created in a public folder" : "{file} was created in a public folder",
"You changed {file}" : "You changed {file}",
"You changed an encrypted file in {file}" : "You changed an encrypted file in {file}",
"{user} changed {file}" : "{user} changed {file}",
"{user} changed an encrypted file in {file}" : "{user} changed an encrypted file in {file}",
+1 -2
View File
@@ -95,7 +95,6 @@ OC.L10N.register(
"Another entry with the same name already exists." : "Інший запис з таким же ім'ям вже присутній",
"Invalid filename." : "Недійсне ім'я файлу.",
"Rename file" : "Перейменувати файл",
"Recently created" : "Нещодавно створено",
"Folder" : "Каталог",
"Unknown file type" : "Невідомий тип файлу",
"{ext} image" : "{ext} зображення",
@@ -234,7 +233,7 @@ OC.L10N.register(
"Changing the file extension from \"{old}\" to \"{new}\" may render the file unreadable." : "Заміна розширення файлу з \"{old}\" на \"{new}\" може зробити файл недоступним.",
"Removing the file extension \"{old}\" may render the file unreadable." : "Вилучення розширення файлу \"{old}\" може зробити файл недоступним.",
"Adding the file extension \"{new}\" may render the file unreadable." : "Додавання розширення до файлу \"{new}\" може зробити файл недоступним.",
"Do not show this dialog again." : "Більше не заптувати",
"Do not show this dialog again." : "Не показувати цей діялог подалі.",
"Rename file to hidden" : "Перейменувати файл у прихований",
"Prefixing a filename with a dot may render the file hidden." : "Якщо додати крапку перед ім'ям файлу зробить файл прихованим.",
"Are you sure you want to rename the file to \"{filename}\"?" : "Дійсно перейменувати файл у \"{filename}\"?",
+1 -2
View File
@@ -93,7 +93,6 @@
"Another entry with the same name already exists." : "Інший запис з таким же ім'ям вже присутній",
"Invalid filename." : "Недійсне ім'я файлу.",
"Rename file" : "Перейменувати файл",
"Recently created" : "Нещодавно створено",
"Folder" : "Каталог",
"Unknown file type" : "Невідомий тип файлу",
"{ext} image" : "{ext} зображення",
@@ -232,7 +231,7 @@
"Changing the file extension from \"{old}\" to \"{new}\" may render the file unreadable." : "Заміна розширення файлу з \"{old}\" на \"{new}\" може зробити файл недоступним.",
"Removing the file extension \"{old}\" may render the file unreadable." : "Вилучення розширення файлу \"{old}\" може зробити файл недоступним.",
"Adding the file extension \"{new}\" may render the file unreadable." : "Додавання розширення до файлу \"{new}\" може зробити файл недоступним.",
"Do not show this dialog again." : "Більше не заптувати",
"Do not show this dialog again." : "Не показувати цей діялог подалі.",
"Rename file to hidden" : "Перейменувати файл у прихований",
"Prefixing a filename with a dot may render the file hidden." : "Якщо додати крапку перед ім'ям файлу зробить файл прихованим.",
"Are you sure you want to rename the file to \"{filename}\"?" : "Дійсно перейменувати файл у \"{filename}\"?",
-6
View File
@@ -95,7 +95,6 @@ OC.L10N.register(
"Another entry with the same name already exists." : "另一相同名称的条目已存在。",
"Invalid filename." : "无效文件名称。",
"Rename file" : "重命名文件",
"Recently created" : "最近创建",
"Folder" : "文件夹",
"Unknown file type" : "未知文件类型",
"{ext} image" : "{ext} 图片",
@@ -235,9 +234,6 @@ OC.L10N.register(
"Removing the file extension \"{old}\" may render the file unreadable." : "删除文件扩展名 \"{old}\" 可能会导致文件无法读取。",
"Adding the file extension \"{new}\" may render the file unreadable." : "添加文件扩展名 \"{new}\" 可能会导致文件无法读取。",
"Do not show this dialog again." : "不再显示此对话框。",
"Rename file to hidden" : "将文件重命名为隐藏文件",
"Prefixing a filename with a dot may render the file hidden." : "在文件名前加上点号可能会使文件变为隐藏文件。",
"Are you sure you want to rename the file to \"{filename}\"?" : "是否确定要将文件重命名为“{filename}”?",
"Cancel" : "取消",
"Rename" : "重命名",
"Select file or folder to link to" : "选择需要链接的文件或文件夹",
@@ -320,9 +316,7 @@ OC.L10N.register(
"The files are locked" : "文件已锁定",
"The file does not exist anymore" : "文件不存在",
"Moving \"{source}\" to \"{destination}\" …" : "正在将“{source}”移动到“{destination}” …",
"Moving {count} files to \"{destination}\" …" : "正在将 {count} 个文件移动到“{destination}” …",
"Copying \"{source}\" to \"{destination}\" …" : "正在将“{source}”复制到“{destination}” …",
"Copying {count} files to \"{destination}\" …" : "正在将 {count} 个文件复制到“{destination}” …",
"Choose destination" : "选择目标路径",
"Copy to {target}" : "复制到 {target}",
"Move to {target}" : "移动到 {target}",
-6
View File
@@ -93,7 +93,6 @@
"Another entry with the same name already exists." : "另一相同名称的条目已存在。",
"Invalid filename." : "无效文件名称。",
"Rename file" : "重命名文件",
"Recently created" : "最近创建",
"Folder" : "文件夹",
"Unknown file type" : "未知文件类型",
"{ext} image" : "{ext} 图片",
@@ -233,9 +232,6 @@
"Removing the file extension \"{old}\" may render the file unreadable." : "删除文件扩展名 \"{old}\" 可能会导致文件无法读取。",
"Adding the file extension \"{new}\" may render the file unreadable." : "添加文件扩展名 \"{new}\" 可能会导致文件无法读取。",
"Do not show this dialog again." : "不再显示此对话框。",
"Rename file to hidden" : "将文件重命名为隐藏文件",
"Prefixing a filename with a dot may render the file hidden." : "在文件名前加上点号可能会使文件变为隐藏文件。",
"Are you sure you want to rename the file to \"{filename}\"?" : "是否确定要将文件重命名为“{filename}”?",
"Cancel" : "取消",
"Rename" : "重命名",
"Select file or folder to link to" : "选择需要链接的文件或文件夹",
@@ -318,9 +314,7 @@
"The files are locked" : "文件已锁定",
"The file does not exist anymore" : "文件不存在",
"Moving \"{source}\" to \"{destination}\" …" : "正在将“{source}”移动到“{destination}” …",
"Moving {count} files to \"{destination}\" …" : "正在将 {count} 个文件移动到“{destination}” …",
"Copying \"{source}\" to \"{destination}\" …" : "正在将“{source}”复制到“{destination}” …",
"Copying {count} files to \"{destination}\" …" : "正在将 {count} 个文件复制到“{destination}” …",
"Choose destination" : "选择目标路径",
"Copy to {target}" : "复制到 {target}",
"Move to {target}" : "移动到 {target}",
+169 -225
View File
@@ -3,259 +3,203 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFolder, INode } from '@nextcloud/files'
import type { FileSource, FilesStore, RootOptions, RootsStore, Service } from '../types.ts'
import type { Folder, Node } from '@nextcloud/files'
import type { FileSource, FilesState, FilesStore, RootOptions, RootsStore, Service } from '../types.ts'
import { subscribe } from '@nextcloud/event-bus'
import { defineStore } from 'pinia'
import Vue, { ref } from 'vue'
import Vue from 'vue'
import logger from '../logger.ts'
import { fetchNode } from '../services/WebdavClient.ts'
import { usePathsStore } from './paths.ts'
/**
* Store for files and folders in the files app.
*
* @param args
*/
export const useFilesStore = defineStore('files', () => {
const files = ref<FilesStore>({})
const roots = ref<RootsStore>({})
export function useFilesStore(...args) {
const store = defineStore('files', {
state: (): FilesState => ({
files: {} as FilesStore,
roots: {} as RootsStore,
}),
// initialize the store once its used first time
initalizeStore()
getters: {
/**
* Get a file or folder by its source
*
* @param state
*/
getNode: (state) => (source: FileSource): Node | undefined => state.files[source],
/**
* Get a file or folder by its source
*
* @param source - The file source
*/
function getNode(source: FileSource): INode | undefined {
return files.value[source]
}
/**
* Get a list of files or folders by their IDs
* Note: does not return undefined values
*
* @param state
*/
getNodes: (state) => (sources: FileSource[]): Node[] => sources
.map((source) => state.files[source])
.filter(Boolean),
/**
* Get a list of files or folders by their IDs
* Note: does not return undefined values
*
* @param sources - The file sources
*/
function getNodes(sources: FileSource[]): INode[] {
return sources
.map((source) => files.value[source])
.filter(Boolean) as INode[]
}
/**
* Get files or folders by their file ID
* Multiple nodes can have the same file ID but different sources
* (e.g. in a shared context)
*
* @param state
*/
getNodesById: (state) => (fileId: number): Node[] => Object.values(state.files).filter((node) => node.fileid === fileId),
/**
* Get files or folders by their ID
* Multiple nodes can have the same ID but different sources
* (e.g. in a shared context)
*
* @param id - The file ID
*/
function getNodesById(id: string): INode[] {
return Object.values(files.value)
.filter((node) => node.id === id)
}
/**
* Get the root folder of a service
*
* @param state
*/
getRoot: (state) => (service: Service): Folder | undefined => state.roots[service],
},
/**
* Get the root folder of a service
*
* @param service - The service (files view)
* @return The root folder if set
*/
function getRoot(service: Service): IFolder | undefined {
return roots.value[service]
}
actions: {
/**
* Get cached directory matching a given path
*
* @param service - The service (files view)
* @param path - The path relative within the service
* @return The folder if found
*/
getDirectoryByPath(service: string, path?: string): Folder | undefined {
const pathsStore = usePathsStore()
let folder: Folder | undefined
/**
* Get cached directory matching a given path
*
* @param service - The service (files view)
* @param path - The path relative within the service
* @return The folder if found
*/
function getDirectoryByPath(service: string, path?: string): IFolder | undefined {
const pathsStore = usePathsStore()
let folder: IFolder | undefined
// Get the containing folder from path store
if (!path || path === '/') {
folder = this.getRoot(service)
} else {
const source = pathsStore.getPath(service, path)
if (source) {
folder = this.getNode(source) as Folder | undefined
}
}
// Get the containing folder from path store
if (!path || path === '/') {
folder = getRoot(service)
} else {
const source = pathsStore.getPath(service, path)
if (source) {
folder = getNode(source) as IFolder | undefined
}
}
return folder
},
return folder
}
/**
* Get cached child nodes within a given path
*
* @param service - The service (files view)
* @param path - The path relative within the service
* @return Array of cached nodes within the path
*/
getNodesByPath(service: string, path?: string): Node[] {
const folder = this.getDirectoryByPath(service, path)
/**
* Get cached child nodes within a given path
*
* @param service - The service (files view)
* @param path - The path relative within the service
* @return Array of cached nodes within the path
*/
function getNodesByPath(service: string, path?: string): INode[] {
const folder = getDirectoryByPath(service, path)
// If we found a cache entry and the cache entry was already loaded (has children) then use it
return (folder?._children ?? [])
.map((source: string) => this.getNode(source))
.filter(Boolean)
},
// If we found a cache entry and the cache entry was already loaded (has children) then use it
return ((folder as { _children?: string[] })?._children ?? [])
.map((source: string) => getNode(source))
.filter(Boolean) as INode[]
}
updateNodes(nodes: Node[]) {
// Update the store all at once
const files = nodes.reduce((acc, node) => {
if (!node.fileid) {
logger.error('Trying to update/set a node without fileid', { node })
return acc
}
/**
* Update or set nodes in the store
*
* @param nodes - The nodes to update or set
*/
function updateNodes(nodes: INode[]) {
// Update the store all at once
const newNodes = nodes.reduce((acc, node) => {
if (files.value[node.source]?.id && !node.id) {
logger.error('Trying to update/set a node without id', { node })
return acc
}
acc[node.source] = node
return acc
}, {} as FilesStore)
acc[node.source] = node
return acc
}, {} as FilesStore)
Vue.set(this, 'files', { ...this.files, ...files })
},
files.value = { ...files.value, ...newNodes }
}
deleteNodes(nodes: Node[]) {
nodes.forEach((node) => {
if (node.source) {
Vue.delete(this.files, node.source)
}
})
},
/**
* Delete nodes from the store
*
* @param nodes - The nodes to delete
*/
function deleteNodes(nodes: INode[]) {
const entries = Object.entries(files.value)
.filter(([, node]) => !nodes.some((n) => n.source === node.source))
files.value = Object.fromEntries(entries)
}
setRoot({ service, root }: RootOptions) {
Vue.set(this.roots, service, root)
},
/**
* Set the root folder for a service
*
* @param options - The options for setting the root
* @param options.service - The service (files view)
* @param options.root - The root folder
*/
function setRoot({ service, root }: RootOptions) {
roots.value = { ...roots.value, [service]: root }
}
onDeletedNode(node: Node) {
this.deleteNodes([node])
},
return {
files,
roots,
onCreatedNode(node: Node) {
this.updateNodes([node])
},
deleteNodes,
getDirectoryByPath,
getNode,
getNodes,
getNodesById,
getNodesByPath,
getRoot,
setRoot,
updateNodes,
}
onMovedNode({ node, oldSource }: { node: Node, oldSource: string }) {
if (!node.fileid) {
logger.error('Trying to update/set a node without fileid', { node })
return
}
// Internal helper functions
// Update the path of the node
Vue.delete(this.files, oldSource)
this.updateNodes([node])
},
/**
* Initialize the store by subscribing to events
*/
function initalizeStore() {
subscribe('files:node:created', onCreatedNode)
subscribe('files:node:deleted', onDeletedNode)
subscribe('files:node:updated', onUpdatedNode)
subscribe('files:node:moved', onMovedNode)
async onUpdatedNode(node: Node) {
if (!node.fileid) {
logger.error('Trying to update/set a node without fileid', { node })
return
}
// If we have multiple nodes with the same file ID, we need to update all of them
const nodes = this.getNodesById(node.fileid)
if (nodes.length > 1) {
await Promise.all(nodes.map((node) => fetchNode(node.path))).then(this.updateNodes)
logger.debug(nodes.length + ' nodes updated in store', { fileid: node.fileid })
return
}
// If we have only one node with the file ID, we can update it directly
if (nodes.length === 1 && node.source === nodes[0].source) {
this.updateNodes([node])
return
}
// Otherwise, it means we receive an event for a node that is not in the store
fetchNode(node.path).then((n) => this.updateNodes([n]))
},
// Handlers for legacy sidebar (no real nodes support)
onAddFavorite(node: Node) {
const ourNode = this.getNode(node.source)
if (ourNode) {
Vue.set(ourNode.attributes, 'favorite', 1)
}
},
onRemoveFavorite(node: Node) {
const ourNode = this.getNode(node.source)
if (ourNode) {
Vue.set(ourNode.attributes, 'favorite', 0)
}
},
},
})
const fileStore = store(...args)
// Make sure we only register the listeners once
if (!fileStore._initialized) {
subscribe('files:node:created', fileStore.onCreatedNode)
subscribe('files:node:deleted', fileStore.onDeletedNode)
subscribe('files:node:updated', fileStore.onUpdatedNode)
subscribe('files:node:moved', fileStore.onMovedNode)
// legacy sidebar
subscribe('files:favorites:added', onAddFavorite)
subscribe('files:favorites:removed', onRemoveFavorite)
subscribe('files:favorites:added', fileStore.onAddFavorite)
subscribe('files:favorites:removed', fileStore.onRemoveFavorite)
fileStore._initialized = true
}
/**
* Called when a node is deleted, removes the node from the store
*
* @param node - The deleted node
*/
function onDeletedNode(node: INode) {
deleteNodes([node])
}
/**
* Handler for when a node is created
*
* @param node - The created node
*/
function onCreatedNode(node: INode) {
updateNodes([node])
}
/**
* Handler for when a node is moved, updates the path of the node in the store
*
* @param context - The context of the moved node
* @param context.node - The moved node
* @param context.oldSource - The old source of the node before it was moved
*/
function onMovedNode({ node, oldSource }: { node: INode, oldSource: string }) {
// Update the path of the node
delete files.value[oldSource]
updateNodes([node])
}
/**
* Handler for when a node is updated, updates the node in the store
*
* @param node - The updated node
*/
async function onUpdatedNode(node: INode) {
// If we have multiple nodes with the same file ID, we need to update all of them
const nodes = node.id
? getNodesById(node.id)
: getNodes([node.source])
if (nodes.length > 1) {
await Promise.all(nodes.map((node) => fetchNode(node.path))).then(updateNodes)
logger.debug(nodes.length + ' nodes updated in store', { fileid: node.id, source: node.source })
return
}
// If we have only one node with the file ID, we can update it directly
if (nodes.length === 1 && node.source === nodes[0]!.source) {
updateNodes([node])
return
}
// Otherwise, it means we receive an event for a node that is not in the store
fetchNode(node.path).then((n) => updateNodes([n]))
}
/**
* Handlers for legacy sidebar (no real nodes support)
*
* @param node - The node that was added to favorites
*/
function onAddFavorite(node: INode) {
const ourNode = getNode(node.source)
if (ourNode) {
Vue.set(ourNode.attributes, 'favorite', 1)
}
}
/**
* Handler for when a node is removed from favorites
*
* @param node - The removed favorite
*/
function onRemoveFavorite(node: INode) {
const ourNode = getNode(node.source)
if (ourNode) {
Vue.set(ourNode.attributes, 'favorite', 0)
}
}
})
return fileStore
}
@@ -33,7 +33,7 @@ const open = defineModel<boolean>('open', { default: true })
const {
storage = { backendOptions: {}, mountOptions: {}, type: isAdmin ? 'system' : 'personal' },
} = defineProps<{
storage?: Partial<IStorage>
storage?: Partial<IStorage> & { backendOptions: IStorage['backendOptions'] }
}>()
defineEmits<{
@@ -88,7 +88,7 @@ watch(authMechanisms, () => {
:label="t('files_external', 'Folder name')"
required />
<MountOptions v-model="internalStorage.mountOptions!" />
<MountOptions v-model="internalStorage.mountOptions" />
<ApplicableEntities
v-if="isAdmin"
@@ -112,13 +112,13 @@ watch(authMechanisms, () => {
required />
<BackendConfiguration
v-if="backend && internalStorage.backendOptions"
v-if="backend"
v-model="internalStorage.backendOptions"
:class="$style.externalStorageDialog__configuration"
:configuration="backend.configuration" />
<AuthMechanismConfiguration
v-if="authMechanism && internalStorage.backendOptions"
v-if="authMechanism"
v-model="internalStorage.backendOptions"
:class="$style.externalStorageDialog__configuration"
:authMechanism="authMechanism" />
@@ -14,14 +14,17 @@ import NcButton from '@nextcloud/vue/components/NcButton'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcSelect from '@nextcloud/vue/components/NcSelect'
import { parseMountOptions } from '../../store/storages.ts'
import { MountOptionsCheckFilesystem } from '../../types.ts'
const mountOptions = defineModel<Partial<IMountOptions>>({ required: true })
watchEffect(() => {
if (Object.keys(mountOptions.value).length === 0) {
// parse and initialize with defaults if needed
mountOptions.value = parseMountOptions(mountOptions.value)
mountOptions.value.encrypt = true
mountOptions.value.previews = true
mountOptions.value.enable_sharing = false
mountOptions.value.filesystem_check_changes = MountOptionsCheckFilesystem.OncePerRequest
mountOptions.value.encoding_compatibility = false
mountOptions.value.readonly = false
}
})
+5 -49
View File
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IStorage } from '../types.ts'
import type { IStorage } from '../types.d.ts'
import axios from '@nextcloud/axios'
import { loadState } from '@nextcloud/initial-state'
@@ -11,7 +11,6 @@ import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextc
import { generateUrl } from '@nextcloud/router'
import { defineStore } from 'pinia'
import { ref, toRaw } from 'vue'
import { MountOptionsCheckFilesystem } from '../types.ts'
const { isAdmin } = loadState<{ isAdmin: boolean }>('files_external', 'settings')
@@ -31,7 +30,7 @@ export const useStorages = defineStore('files_external--storages', () => {
toRaw(storage),
{ confirmPassword: PwdConfirmationMode.Strict },
)
globalStorages.value.push(parseStorage(data))
globalStorages.value.push(data)
}
/**
@@ -46,7 +45,7 @@ export const useStorages = defineStore('files_external--storages', () => {
toRaw(storage),
{ confirmPassword: PwdConfirmationMode.Strict },
)
userStorages.value.push(parseStorage(data))
userStorages.value.push(data)
}
/**
@@ -78,7 +77,7 @@ export const useStorages = defineStore('files_external--storages', () => {
{ confirmPassword: PwdConfirmationMode.Strict },
)
overrideStorage(parseStorage(data))
overrideStorage(data)
}
/**
@@ -88,7 +87,7 @@ export const useStorages = defineStore('files_external--storages', () => {
*/
async function reloadStorage(storage: IStorage) {
const { data } = await axios.get(getUrl(storage))
overrideStorage(parseStorage(data))
overrideStorage(data)
}
// initialize the store
@@ -112,7 +111,6 @@ export const useStorages = defineStore('files_external--storages', () => {
const url = `apps/files_external/${type}`
const { data } = await axios.get<Record<number, IStorage>>(generateUrl(url))
return Object.values(data)
.map(parseStorage)
}
/**
@@ -152,45 +150,3 @@ export const useStorages = defineStore('files_external--storages', () => {
}
}
})
/**
* @param storage - The storage from API
*/
function parseStorage(storage: IStorage) {
return {
...storage,
mountOptions: parseMountOptions(storage.mountOptions),
}
}
/**
* Parse the mount options and convert string boolean values to
* actual booleans and numeric strings to numbers
*
* @param options - The mount options to parse
*/
export function parseMountOptions(options: IStorage['mountOptions']) {
const mountOptions = { ...options }
mountOptions.encrypt = convertBooleanOptions(mountOptions.encrypt, true)
mountOptions.previews = convertBooleanOptions(mountOptions.previews, true)
mountOptions.enable_sharing = convertBooleanOptions(mountOptions.enable_sharing, false)
mountOptions.filesystem_check_changes = typeof mountOptions.filesystem_check_changes === 'string'
? Number.parseInt(mountOptions.filesystem_check_changes)
: (mountOptions.filesystem_check_changes ?? MountOptionsCheckFilesystem.OncePerRequest)
mountOptions.encoding_compatibility = convertBooleanOptions(mountOptions.encoding_compatibility, false)
mountOptions.readonly = convertBooleanOptions(mountOptions.readonly, false)
return mountOptions
}
/**
* Convert backend encoding of boolean options
*
* @param option - The option value from API
* @param fallback - The fallback (default) value
*/
function convertBooleanOptions(option: unknown, fallback = false) {
if (option === undefined) {
return fallback
}
return option === true || option === 'true' || option === '1'
}
@@ -59,8 +59,7 @@ async function addStorage(storage?: Partial<IStorage>) {
}
newStorage.value = undefined
} catch (error) {
logger.error('Failed to add external storage', { error, storage })
newStorage.value = { ...storage }
logger.error('Failed to add external storage', { error })
showDialog.value = true
}
}
@@ -135,8 +134,8 @@ async function addStorage(storage?: Partial<IStorage>) {
</NcButton>
<AddExternalStorageDialog
v-model="newStorage"
v-model:open="showDialog"
:storage="newStorage"
@close="addStorage" />
<UserMountSettings v-if="settings.isAdmin" />
@@ -3,7 +3,8 @@
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+1 -1
View File
@@ -149,7 +149,7 @@ OC.L10N.register(
"Select" : "Auswählen",
"What are you requesting?" : "Was fragst du an?",
"Request subject" : "Betreff der Anfrage",
"Birthday party photos, History assignment…" : "Fotos von Geburtstagsfeiern, Geschichtsaufgaben …",
"Birthday party photos, History assignment…" : "Fotos von Geburtstagsfeiern, Geschichtsaufgaben…",
"Where should these files go?" : "Wo sollen diese Dateien gespeichert werden?",
"Upload destination" : "Ziel für das Hochladen",
"Revert to default" : "Auf Standard zurücksetzen",
+1 -1
View File
@@ -147,7 +147,7 @@
"Select" : "Auswählen",
"What are you requesting?" : "Was fragst du an?",
"Request subject" : "Betreff der Anfrage",
"Birthday party photos, History assignment…" : "Fotos von Geburtstagsfeiern, Geschichtsaufgaben …",
"Birthday party photos, History assignment…" : "Fotos von Geburtstagsfeiern, Geschichtsaufgaben…",
"Where should these files go?" : "Wo sollen diese Dateien gespeichert werden?",
"Upload destination" : "Ziel für das Hochladen",
"Revert to default" : "Auf Standard zurücksetzen",
+2 -2
View File
@@ -149,7 +149,7 @@ OC.L10N.register(
"Select" : "Auswählen",
"What are you requesting?" : "Was fragen Sie an?",
"Request subject" : "Betreff der Anfrage",
"Birthday party photos, History assignment…" : "Fotos von Geburtstagsfeiern, Geschichtsaufgaben …",
"Birthday party photos, History assignment…" : "Fotos von Geburtstagsfeiern, Geschichtsaufgaben…",
"Where should these files go?" : "Wo sollen diese Dateien gespeichert werden?",
"Upload destination" : "Ziel für das Hochladen",
"Revert to default" : "Auf Standard zurücksetzen",
@@ -283,7 +283,7 @@ OC.L10N.register(
"Share label" : "Freigabe-Label",
"Share link token" : "Freigabe-Token teilen",
"Set the public share link token to something easy to remember or generate a new token. It is not recommended to use a guessable token for shares which contain sensitive information." : "Das öffentliche Freigabelink-Token auf einen Begriff festlegen, der leicht zu merken ist, oder ein neues Token erstellen. Es ist nicht zu empfehlen, für Freigaben , die vertrauliche Informationen enthalten, ein erratbares Token zu verwenden.",
"Generating…" : "Generieren …",
"Generating…" : "Generieren…",
"Generate new token" : "Neues Token generieren",
"Set password" : "Passwort festlegen",
"Password expires {passwordExpirationTime}" : "Passwort läuft ab um {passwordExpirationTime}",
+2 -2
View File
@@ -147,7 +147,7 @@
"Select" : "Auswählen",
"What are you requesting?" : "Was fragen Sie an?",
"Request subject" : "Betreff der Anfrage",
"Birthday party photos, History assignment…" : "Fotos von Geburtstagsfeiern, Geschichtsaufgaben …",
"Birthday party photos, History assignment…" : "Fotos von Geburtstagsfeiern, Geschichtsaufgaben…",
"Where should these files go?" : "Wo sollen diese Dateien gespeichert werden?",
"Upload destination" : "Ziel für das Hochladen",
"Revert to default" : "Auf Standard zurücksetzen",
@@ -281,7 +281,7 @@
"Share label" : "Freigabe-Label",
"Share link token" : "Freigabe-Token teilen",
"Set the public share link token to something easy to remember or generate a new token. It is not recommended to use a guessable token for shares which contain sensitive information." : "Das öffentliche Freigabelink-Token auf einen Begriff festlegen, der leicht zu merken ist, oder ein neues Token erstellen. Es ist nicht zu empfehlen, für Freigaben , die vertrauliche Informationen enthalten, ein erratbares Token zu verwenden.",
"Generating…" : "Generieren …",
"Generating…" : "Generieren…",
"Generate new token" : "Neues Token generieren",
"Set password" : "Passwort festlegen",
"Password expires {passwordExpirationTime}" : "Passwort läuft ab um {passwordExpirationTime}",
+1 -1
View File
@@ -265,7 +265,7 @@ OC.L10N.register(
"Share in conversation" : "Zdieľať v rozhovore",
"Share with {user} on remote server {server}" : "Sprístupniť s {user} na vzdialenom servery {server}",
"Share with remote group" : "Zdieľať so vzdialenou skupinou",
"Share with guest" : "Zdieľať s hosťom",
"Share with guest" : "Zdiľať s hosťom",
"Update share" : "Aktualizovať zdieľanie",
"Save share" : "Uložiť zdieľanie",
"Read" : "Čítať",
+1 -1
View File
@@ -263,7 +263,7 @@
"Share in conversation" : "Zdieľať v rozhovore",
"Share with {user} on remote server {server}" : "Sprístupniť s {user} na vzdialenom servery {server}",
"Share with remote group" : "Zdieľať so vzdialenou skupinou",
"Share with guest" : "Zdieľať s hosťom",
"Share with guest" : "Zdiľať s hosťom",
"Update share" : "Aktualizovať zdieľanie",
"Save share" : "Uložiť zdieľanie",
"Read" : "Čítať",
@@ -153,7 +153,7 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider
// Create the header action menu
$headerActions = [];
if ($share->canDownload() && !$share->getHideDownload()) {
if ($view !== 'public-file-drop' && !$share->getHideDownload()) {
// The download URL is used for the "download" header action as well as in some cases for the direct link
$downloadUrl = $this->urlGenerator->getAbsoluteURL('/public.php/dav/files/' . $token . '/?accept=zip');
+2 -1
View File
@@ -3,7 +3,8 @@
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+2 -1
View File
@@ -3,7 +3,8 @@
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+1 -1
View File
@@ -468,7 +468,7 @@ class SharedStorage extends Jail implements LegacyISharedStorage, ISharedStorage
*/
public function unshareStorage(): bool {
foreach ($this->groupedShares as $share) {
Server::get(IShareManager::class)->deleteFromSelf($share, $this->user);
Server::get(\OCP\Share\IManager::class)->deleteFromSelf($share, $this->user);
}
return true;
}
+1 -4
View File
@@ -180,10 +180,7 @@ export default {
async set(enabled) {
if (enabled) {
this.passwordProtectedState = true
const generatedPassword = await GeneratePassword(true)
if (!this.share.newPassword) {
this.$set(this.share, 'newPassword', generatedPassword)
}
this.$set(this.share, 'newPassword', await GeneratePassword(true))
} else {
this.passwordProtectedState = false
this.$set(this.share, 'newPassword', '')
@@ -1,301 +0,0 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockGeneratePassword = vi.fn().mockResolvedValue('generated-password-123')
vi.mock('../services/ConfigService.ts', () => ({
default: vi.fn().mockImplementation(() => ({
enableLinkPasswordByDefault: false,
enforcePasswordForPublicLink: false,
isPublicUploadEnabled: true,
isDefaultExpireDateEnabled: false,
isDefaultInternalExpireDateEnabled: false,
isDefaultRemoteExpireDateEnabled: false,
defaultExpirationDate: null,
defaultInternalExpirationDate: null,
defaultRemoteExpirationDateString: null,
isResharingAllowed: true,
excludeReshareFromEdit: false,
showFederatedSharesAsInternal: false,
defaultPermissions: 31,
})),
}))
vi.mock('../utils/GeneratePassword.ts', () => ({
default: (...args: unknown[]) => mockGeneratePassword(...args),
}))
/**
* Simulates the isPasswordProtected getter from SharesMixin.js
*/
function getIsPasswordProtected(state: {
enforcePasswordForPublicLink: boolean
passwordProtectedState: boolean | undefined
newPassword: string | undefined
password: string | undefined
}): boolean {
if (state.enforcePasswordForPublicLink) {
return true
}
if (state.passwordProtectedState !== undefined) {
return state.passwordProtectedState
}
return typeof state.newPassword === 'string'
|| typeof state.password === 'string'
}
/**
* Simulates the isPasswordProtected setter from SharesMixin.js
* Returns the resulting share state after the async operation completes.
*/
async function setIsPasswordProtected(
enabled: boolean,
share: { newPassword?: string },
): Promise<{ passwordProtectedState: boolean, share: { newPassword?: string } }> {
if (enabled) {
const generatedPassword = await mockGeneratePassword(true)
if (!share.newPassword) {
share.newPassword = generatedPassword
}
return { passwordProtectedState: true, share }
} else {
share.newPassword = ''
return { passwordProtectedState: false, share }
}
}
describe('SharingDetailsTab - Password State Management Logic', () => {
beforeEach(() => {
mockGeneratePassword.mockClear()
mockGeneratePassword.mockResolvedValue('generated-password-123')
})
describe('isPasswordProtected getter', () => {
it('returns true when enforcePasswordForPublicLink is true regardless of other state', () => {
expect(getIsPasswordProtected({
enforcePasswordForPublicLink: true,
passwordProtectedState: false,
newPassword: undefined,
password: undefined,
})).toBe(true)
})
it('returns true when passwordProtectedState is explicitly true', () => {
expect(getIsPasswordProtected({
enforcePasswordForPublicLink: false,
passwordProtectedState: true,
newPassword: undefined,
password: undefined,
})).toBe(true)
})
it('returns false when passwordProtectedState is explicitly false', () => {
expect(getIsPasswordProtected({
enforcePasswordForPublicLink: false,
passwordProtectedState: false,
newPassword: 'some-password',
password: undefined,
})).toBe(false)
})
it('falls back to inferring from newPassword when passwordProtectedState is undefined', () => {
expect(getIsPasswordProtected({
enforcePasswordForPublicLink: false,
passwordProtectedState: undefined,
newPassword: 'some-password',
password: undefined,
})).toBe(true)
})
it('falls back to inferring from password when passwordProtectedState is undefined', () => {
expect(getIsPasswordProtected({
enforcePasswordForPublicLink: false,
passwordProtectedState: undefined,
newPassword: undefined,
password: 'existing-password',
})).toBe(true)
})
it('returns false when passwordProtectedState is undefined and no passwords exist', () => {
expect(getIsPasswordProtected({
enforcePasswordForPublicLink: false,
passwordProtectedState: undefined,
newPassword: undefined,
password: undefined,
})).toBe(false)
})
it('checkbox remains checked when passwordProtectedState is true even if password is cleared', () => {
expect(getIsPasswordProtected({
enforcePasswordForPublicLink: false,
passwordProtectedState: true,
newPassword: '',
password: undefined,
})).toBe(true)
})
})
describe('isPasswordProtected setter (race condition fix)', () => {
it('generated password does NOT overwrite user-typed password', async () => {
const share = { newPassword: 'user-typed-password' }
const result = await setIsPasswordProtected(true, share)
expect(mockGeneratePassword).toHaveBeenCalledWith(true)
expect(result.passwordProtectedState).toBe(true)
expect(result.share.newPassword).toBe('user-typed-password')
})
it('generated password IS applied when user has not typed anything', async () => {
const share: { newPassword?: string } = {}
const result = await setIsPasswordProtected(true, share)
expect(mockGeneratePassword).toHaveBeenCalledWith(true)
expect(result.passwordProtectedState).toBe(true)
expect(result.share.newPassword).toBe('generated-password-123')
})
it('generated password IS applied when newPassword is empty string (user cleared input)', async () => {
const share = { newPassword: '' }
const result = await setIsPasswordProtected(true, share)
expect(result.share.newPassword).toBe('generated-password-123')
})
it('disabling password clears newPassword and sets state to false', async () => {
const share = { newPassword: 'some-password' }
const result = await setIsPasswordProtected(false, share)
expect(result.passwordProtectedState).toBe(false)
expect(result.share.newPassword).toBe('')
})
})
describe('initializeAttributes sets passwordProtectedState', () => {
it('should set passwordProtectedState when enableLinkPasswordByDefault is true for new public share', () => {
const config = { enableLinkPasswordByDefault: true, enforcePasswordForPublicLink: false }
const isNewShare = true
const isPublicShare = true
let passwordProtectedState: boolean | undefined
if (isNewShare && (config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) {
passwordProtectedState = true
}
expect(passwordProtectedState).toBe(true)
})
it('should set passwordProtectedState when isPasswordEnforced is true for new public share', () => {
const config = { enableLinkPasswordByDefault: false, enforcePasswordForPublicLink: true }
const isNewShare = true
const isPublicShare = true
let passwordProtectedState: boolean | undefined
if (isNewShare && (config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) {
passwordProtectedState = true
}
expect(passwordProtectedState).toBe(true)
})
it('should not set passwordProtectedState for non-public shares', () => {
const config = { enableLinkPasswordByDefault: true, enforcePasswordForPublicLink: false }
const isNewShare = true
const isPublicShare = false
let passwordProtectedState: boolean | undefined
if (isNewShare && (config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) {
passwordProtectedState = true
}
expect(passwordProtectedState).toBe(undefined)
})
it('should not set passwordProtectedState for existing shares', () => {
const config = { enableLinkPasswordByDefault: true, enforcePasswordForPublicLink: false }
const isNewShare = false
const isPublicShare = true
let passwordProtectedState: boolean | undefined
if (isNewShare && (config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) {
passwordProtectedState = true
}
expect(passwordProtectedState).toBe(undefined)
})
})
describe('saveShare validation blocks empty password', () => {
const isValidShareAttribute = (attr: unknown) => {
return typeof attr === 'string' && attr.length > 0
}
it('should set passwordError when isPasswordProtected but newPassword is empty for new share', () => {
const isPasswordProtected = true
const isNewShare = true
const newPassword = ''
let passwordError = false
if (isPasswordProtected && isNewShare && !isValidShareAttribute(newPassword)) {
passwordError = true
}
expect(passwordError).toBe(true)
})
it('should set passwordError when isPasswordProtected but newPassword is undefined for new share', () => {
const isPasswordProtected = true
const isNewShare = true
const newPassword = undefined
let passwordError = false
if (isPasswordProtected && isNewShare && !isValidShareAttribute(newPassword)) {
passwordError = true
}
expect(passwordError).toBe(true)
})
it('should not set passwordError when password is valid for new share', () => {
const isPasswordProtected = true
const isNewShare = true
const newPassword = 'valid-password-123'
let passwordError = false
if (isPasswordProtected && isNewShare && !isValidShareAttribute(newPassword)) {
passwordError = true
}
expect(passwordError).toBe(false)
})
it('should not set passwordError when isPasswordProtected is false', () => {
const isPasswordProtected = false
const isNewShare = true
const newPassword = ''
let passwordError = false
if (isPasswordProtected && isNewShare && !isValidShareAttribute(newPassword)) {
passwordError = true
}
expect(passwordError).toBe(false)
})
it('should not validate password for existing shares', () => {
const isPasswordProtected = true
const isNewShare = false
const newPassword = ''
let passwordError = false
if (isPasswordProtected && isNewShare && !isValidShareAttribute(newPassword)) {
passwordError = true
}
expect(passwordError).toBe(false)
})
})
})
@@ -974,11 +974,7 @@ export default {
async initializeAttributes() {
if (this.isNewShare) {
if ((this.config.enableLinkPasswordByDefault || this.isPasswordEnforced) && this.isPublicShare) {
this.passwordProtectedState = true
const generatedPassword = await GeneratePassword(true)
if (!this.share.newPassword) {
this.$set(this.share, 'newPassword', generatedPassword)
}
this.$set(this.share, 'newPassword', await GeneratePassword(true))
this.advancedSectionAccordionExpanded = true
}
/* Set default expiration dates if configured */
@@ -1091,9 +1087,8 @@ export default {
this.share.note = ''
}
if (this.isPasswordProtected) {
if (this.isNewShare && !this.isValidShareAttribute(this.share.newPassword)) {
if (this.isPasswordEnforced && this.isNewShare && !this.isValidShareAttribute(this.share.newPassword)) {
this.passwordError = true
return
}
} else {
this.share.password = ''
@@ -93,7 +93,7 @@ class OauthApiController extends Controller {
$response = new JSONResponse([
'error' => 'invalid_request',
], Http::STATUS_BAD_REQUEST);
$response->throttle(['invalid_request' => 'token not found']);
$response->throttle(['invalid_request' => 'token not found', 'code' => $code]);
return $response;
}
@@ -98,7 +98,7 @@ class OauthApiControllerTest extends TestCase {
$expected = new JSONResponse([
'error' => 'invalid_request',
], Http::STATUS_BAD_REQUEST);
$expected->throttle(['invalid_request' => 'token not found']);
$expected->throttle(['invalid_request' => 'token not found', 'code' => 'invalidcode']);
$this->accessTokenMapper->method('getByCode')
->with('invalidcode')
@@ -194,7 +194,7 @@ class OauthApiControllerTest extends TestCase {
$expected = new JSONResponse([
'error' => 'invalid_request',
], Http::STATUS_BAD_REQUEST);
$expected->throttle(['invalid_request' => 'token not found']);
$expected->throttle(['invalid_request' => 'token not found', 'code' => 'invalidrefresh']);
$this->accessTokenMapper->method('getByCode')
->with('invalidrefresh')
@@ -11,7 +11,6 @@ namespace OCA\Provisioning_API\Middleware;
use OCA\Provisioning_API\Middleware\Exceptions\NotSubAdminException;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Middleware;
use OCP\AppFramework\OCS\OCSException;
@@ -41,7 +40,7 @@ class ProvisioningApiMiddleware extends Middleware {
*/
public function beforeController($controller, $methodName) {
// If AuthorizedAdminSetting, the check will be done in the SecurityMiddleware
if (!$this->isAdmin && !$this->reflector->hasAnnotation('NoSubAdminRequired') && !$this->isSubAdmin && !$this->reflector->hasAnnotationOrAttribute('AuthorizedAdminSetting', AuthorizedAdminSetting::class)) {
if (!$this->isAdmin && !$this->reflector->hasAnnotation('NoSubAdminRequired') && !$this->isSubAdmin && !$this->reflector->hasAnnotation('AuthorizedAdminSetting')) {
throw new NotSubAdminException();
}
}
@@ -53,14 +53,10 @@ class ProvisioningApiMiddlewareTest extends TestCase {
);
$this->reflector->method('hasAnnotation')
->willReturnCallback(function ($annotation) use ($subadminRequired) {
->willReturnCallback(function ($annotation) use ($subadminRequired, $hasSettingAuthorizationAnnotation) {
if ($annotation === 'NoSubAdminRequired') {
return !$subadminRequired;
}
return false;
});
$this->reflector->method('hasAnnotationOrAttribute')
->willReturnCallback(function ($annotation, $attribute) use ($hasSettingAuthorizationAnnotation) {
if ($annotation === 'AuthorizedAdminSetting') {
return $hasSettingAuthorizationAnnotation;
}
-3
View File
@@ -593,11 +593,8 @@ OC.L10N.register(
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 accounts depending on the usage)." : "cron.php je zaregistrován u služby webcron aby přes HTTP volala cron.php každých 5 minut. Příklad použití: velmi malá instance (1-5 účtů, v závislosti na vytížení).",
"Cron (Recommended)" : "Cron (doporučeno)",
"Unable to update profile default setting" : "Nedaří se aktualizovat výchozí nastavení pro profily",
"Unable to update profile picker setting" : "Nebylo možné zaktualizovat nastavení výběru profilu",
"Profile" : "Profil",
"Enable or disable profile by default for new accounts." : "Profily nově vytvářených účtů ve výchozím stavu zpřístupňovat nebo nezpřístupňovat.",
"Enable the profile picker" : "Zapnout nástroj pro výběr profilu",
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Zapnout nebo vypnout nástroj pro výběr profilů v Inteligentním výběru a náhledech odkazu na profil.",
"Password confirmation is required" : "Je vyžadováno potvrzení hesla",
"Failed to save setting" : "Nastavení se nepodařilo uložit",
"{app}'s declarative setting field: {name}" : "Kolonka deklarativního nastavení {app}: {name}",
-3
View File
@@ -591,11 +591,8 @@
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 accounts depending on the usage)." : "cron.php je zaregistrován u služby webcron aby přes HTTP volala cron.php každých 5 minut. Příklad použití: velmi malá instance (1-5 účtů, v závislosti na vytížení).",
"Cron (Recommended)" : "Cron (doporučeno)",
"Unable to update profile default setting" : "Nedaří se aktualizovat výchozí nastavení pro profily",
"Unable to update profile picker setting" : "Nebylo možné zaktualizovat nastavení výběru profilu",
"Profile" : "Profil",
"Enable or disable profile by default for new accounts." : "Profily nově vytvářených účtů ve výchozím stavu zpřístupňovat nebo nezpřístupňovat.",
"Enable the profile picker" : "Zapnout nástroj pro výběr profilu",
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Zapnout nebo vypnout nástroj pro výběr profilů v Inteligentním výběru a náhledech odkazu na profil.",
"Password confirmation is required" : "Je vyžadováno potvrzení hesla",
"Failed to save setting" : "Nastavení se nepodařilo uložit",
"{app}'s declarative setting field: {name}" : "Kolonka deklarativního nastavení {app}: {name}",
+16 -16
View File
@@ -118,7 +118,7 @@ OC.L10N.register(
"Background jobs" : "Hintergrundaufgaben",
"Unlimited" : "Unbegrenzt",
"Verifying" : "Überprüfe",
"Verifying …" : "Überprüfe …",
"Verifying …" : "Überprüfe…",
"Verify" : "Überprüfen",
"Allowed admin IP ranges" : "Zulässige Administrations-IP-Bereiche",
"Admin IP filtering isn't applied." : "IP-Filterung durch Administration ist nicht aktiv.",
@@ -139,8 +139,8 @@ OC.L10N.register(
"Integrity checker has been disabled. Integrity cannot be verified." : "Die Integritätsprüfung wurde deaktiviert. Die Integrität kann nicht überprüft werden.",
"No altered files" : "Keine veränderten Dateien",
"Some files have not passed the integrity check. {link1} {link2}" : "Einige Dateien haben die Integritätsprüfung nicht bestanden. {link1} {link2}",
"List of invalid files…" : "Liste ungültiger Dateien …",
"Rescan…" : "Erneut scannen …",
"List of invalid files…" : "Liste ungültiger Dateien…",
"Rescan…" : "Erneut scannen…",
"Cron errors" : "Cron-Fehler",
"It was not possible to execute the cron job via CLI. The following technical errors have appeared:\n%s" : "Es war nicht möglich, den Cron-Job über die CLI auszuführen. Es sind folgende technische Fehler aufgetreten:\n%s",
"The last cron job ran without errors." : "Der letzte Cron-Job wurde ohne Fehler ausgeführt.",
@@ -465,19 +465,19 @@ OC.L10N.register(
"Failed to load groups" : "Gruppen konnten nicht geladen werden",
"Failed to create group" : "Gruppe konnte nicht erstellt werden",
"Groups" : "Gruppen",
"Creating group…" : "Erstelle Gruppe …",
"Creating group…" : "Erstelle Gruppe…",
"Create group" : "Gruppe erstellen",
"Group name" : "Gruppenname",
"Please enter a valid group name" : "Bitte einen gültigen Gruppennamen eingeben",
"Search groups…" : "Suche Gruppen …",
"Search groups…" : "Suche Gruppen…",
"List of groups. This list is not fully populated for performance reasons. The groups will be loaded as you navigate or search through the list." : "Liste der Gruppen. Diese Liste ist aus Leistungsgründen nicht vollständig gefüllt. Die Gruppen werden während des Navigierens oder Suchens geladen.",
"Loading groups…" : "Lade Gruppen …",
"Loading groups…" : "Lade Gruppen…",
"Could not load app discover section" : "Der Menüpunkt \"Endecken\" konnte nicht geladen werden.",
"Could not render element" : "Element konnte nicht dargestellt werden",
"Nothing to show" : "Nichts anzuzeigen",
"Could not load section content from app store." : "Abschnittsinhalt konnte nicht aus dem App Store geladen werden.",
"Loading" : "Lade",
"Fetching the latest news…" : "Aktuelle Nachrichten werden abgerufen …",
"Fetching the latest news…" : "Aktuelle Nachrichten werden abgerufen…",
"Carousel" : "Karussell",
"Previous slide" : "Vorherige Folie",
"Next slide" : "Nächste Folie",
@@ -849,7 +849,7 @@ OC.L10N.register(
"Authentication required" : "Authentifizierung benötigt",
"Sending test email…" : "Sende Test-E-Mail …",
"Send test email" : "Test-E-Mail senden",
"Saving…" : "Speichere …",
"Saving…" : "Speichere …",
"Save settings" : "Einstellungen speichern",
"Please double check the {linkStartInstallationGuides}installation guides{linkEnd}, and check for any errors or warnings in the {linkStartLog}log{linkEnd}." : "Bitte die {linkStartInstallationGuides} Installationsanleitung {linkEnd} nochmals überprüfen und im {linkStartLog}Protokoll{linkEnd} nach Fehlern oder Warnungen sehen.",
"Check the security of your {productName} over {linkStart}our security scan{linkEnd}." : "Die Sicherheit von {productName} mit {linkStart}unserem Sicherheitsscan{linkEnd} überprüfen",
@@ -985,15 +985,15 @@ OC.L10N.register(
"Exclude some groups from sharing" : "Bestimmte Gruppen vom Teilen ausschließen",
"Limit sharing to some groups" : "Teilen für bestimmte Gruppen erlauben",
"Also allow autocompletion on full match of the user id" : "Automatische Vervollständigung auch bei vollständiger Übereinstimmung der Benutzer-ID zulassen",
"Loading accounts …" : "Lade Konten …",
"Set account as admin for …" : "Konto als Administration setzen für …",
"_{userCount} account …_::_{userCount} accounts …_" : ["{userCount} Konto …","{userCount} Konten …"],
"Loading account …" : "Lade Konto …",
"Adding your device …" : "Dieses Gerät hinzufügen …",
"Sending…" : "Sende …",
"Loading accounts …" : "Lade Konten …",
"Set account as admin for …" : "Konto als Administration setzen für …",
"_{userCount} account …_::_{userCount} accounts …_" : ["{userCount} Konto …","{userCount} Konten …"],
"Loading account …" : "Lade Konto …",
"Adding your device …" : "Dieses Gerät hinzufügen …",
"Sending…" : "Sende…",
"Email sent" : "E-Mail gesendet",
"{progress}% Deploying …" : "{progress}% bereitstellen …",
"{progress}% Initializing …" : "{progress}% initialisiere …",
"{progress}% Deploying …" : "{progress}% Bereitstellen …",
"{progress}% Initializing …" : "{progress}% Initialisierung …",
"None/STARTTLS" : "Keine/STARTTLS",
"SSL" : "SSL",
"Credentials" : "Zugangsdaten",
+16 -16
View File
@@ -116,7 +116,7 @@
"Background jobs" : "Hintergrundaufgaben",
"Unlimited" : "Unbegrenzt",
"Verifying" : "Überprüfe",
"Verifying …" : "Überprüfe …",
"Verifying …" : "Überprüfe…",
"Verify" : "Überprüfen",
"Allowed admin IP ranges" : "Zulässige Administrations-IP-Bereiche",
"Admin IP filtering isn't applied." : "IP-Filterung durch Administration ist nicht aktiv.",
@@ -137,8 +137,8 @@
"Integrity checker has been disabled. Integrity cannot be verified." : "Die Integritätsprüfung wurde deaktiviert. Die Integrität kann nicht überprüft werden.",
"No altered files" : "Keine veränderten Dateien",
"Some files have not passed the integrity check. {link1} {link2}" : "Einige Dateien haben die Integritätsprüfung nicht bestanden. {link1} {link2}",
"List of invalid files…" : "Liste ungültiger Dateien …",
"Rescan…" : "Erneut scannen …",
"List of invalid files…" : "Liste ungültiger Dateien…",
"Rescan…" : "Erneut scannen…",
"Cron errors" : "Cron-Fehler",
"It was not possible to execute the cron job via CLI. The following technical errors have appeared:\n%s" : "Es war nicht möglich, den Cron-Job über die CLI auszuführen. Es sind folgende technische Fehler aufgetreten:\n%s",
"The last cron job ran without errors." : "Der letzte Cron-Job wurde ohne Fehler ausgeführt.",
@@ -463,19 +463,19 @@
"Failed to load groups" : "Gruppen konnten nicht geladen werden",
"Failed to create group" : "Gruppe konnte nicht erstellt werden",
"Groups" : "Gruppen",
"Creating group…" : "Erstelle Gruppe …",
"Creating group…" : "Erstelle Gruppe…",
"Create group" : "Gruppe erstellen",
"Group name" : "Gruppenname",
"Please enter a valid group name" : "Bitte einen gültigen Gruppennamen eingeben",
"Search groups…" : "Suche Gruppen …",
"Search groups…" : "Suche Gruppen…",
"List of groups. This list is not fully populated for performance reasons. The groups will be loaded as you navigate or search through the list." : "Liste der Gruppen. Diese Liste ist aus Leistungsgründen nicht vollständig gefüllt. Die Gruppen werden während des Navigierens oder Suchens geladen.",
"Loading groups…" : "Lade Gruppen …",
"Loading groups…" : "Lade Gruppen…",
"Could not load app discover section" : "Der Menüpunkt \"Endecken\" konnte nicht geladen werden.",
"Could not render element" : "Element konnte nicht dargestellt werden",
"Nothing to show" : "Nichts anzuzeigen",
"Could not load section content from app store." : "Abschnittsinhalt konnte nicht aus dem App Store geladen werden.",
"Loading" : "Lade",
"Fetching the latest news…" : "Aktuelle Nachrichten werden abgerufen …",
"Fetching the latest news…" : "Aktuelle Nachrichten werden abgerufen…",
"Carousel" : "Karussell",
"Previous slide" : "Vorherige Folie",
"Next slide" : "Nächste Folie",
@@ -847,7 +847,7 @@
"Authentication required" : "Authentifizierung benötigt",
"Sending test email…" : "Sende Test-E-Mail …",
"Send test email" : "Test-E-Mail senden",
"Saving…" : "Speichere …",
"Saving…" : "Speichere …",
"Save settings" : "Einstellungen speichern",
"Please double check the {linkStartInstallationGuides}installation guides{linkEnd}, and check for any errors or warnings in the {linkStartLog}log{linkEnd}." : "Bitte die {linkStartInstallationGuides} Installationsanleitung {linkEnd} nochmals überprüfen und im {linkStartLog}Protokoll{linkEnd} nach Fehlern oder Warnungen sehen.",
"Check the security of your {productName} over {linkStart}our security scan{linkEnd}." : "Die Sicherheit von {productName} mit {linkStart}unserem Sicherheitsscan{linkEnd} überprüfen",
@@ -983,15 +983,15 @@
"Exclude some groups from sharing" : "Bestimmte Gruppen vom Teilen ausschließen",
"Limit sharing to some groups" : "Teilen für bestimmte Gruppen erlauben",
"Also allow autocompletion on full match of the user id" : "Automatische Vervollständigung auch bei vollständiger Übereinstimmung der Benutzer-ID zulassen",
"Loading accounts …" : "Lade Konten …",
"Set account as admin for …" : "Konto als Administration setzen für …",
"_{userCount} account …_::_{userCount} accounts …_" : ["{userCount} Konto …","{userCount} Konten …"],
"Loading account …" : "Lade Konto …",
"Adding your device …" : "Dieses Gerät hinzufügen …",
"Sending…" : "Sende …",
"Loading accounts …" : "Lade Konten …",
"Set account as admin for …" : "Konto als Administration setzen für …",
"_{userCount} account …_::_{userCount} accounts …_" : ["{userCount} Konto …","{userCount} Konten …"],
"Loading account …" : "Lade Konto …",
"Adding your device …" : "Dieses Gerät hinzufügen …",
"Sending…" : "Sende…",
"Email sent" : "E-Mail gesendet",
"{progress}% Deploying …" : "{progress}% bereitstellen …",
"{progress}% Initializing …" : "{progress}% initialisiere …",
"{progress}% Deploying …" : "{progress}% Bereitstellen …",
"{progress}% Initializing …" : "{progress}% Initialisierung …",
"None/STARTTLS" : "Keine/STARTTLS",
"SSL" : "SSL",
"Credentials" : "Zugangsdaten",
+15 -15
View File
@@ -118,7 +118,7 @@ OC.L10N.register(
"Background jobs" : "Hintergrundaufgaben",
"Unlimited" : "Unbegrenzt",
"Verifying" : "Überprüfe",
"Verifying …" : "Überprüfe …",
"Verifying …" : "Überprüfe…",
"Verify" : "Überprüfen",
"Allowed admin IP ranges" : "Zulässige Administrations-IP-Bereiche",
"Admin IP filtering isn't applied." : "IP-Filterung durch Administration ist nicht aktiv.",
@@ -139,8 +139,8 @@ OC.L10N.register(
"Integrity checker has been disabled. Integrity cannot be verified." : "Die Integritätsprüfung wurde deaktiviert. Die Integrität kann nicht überprüft werden.",
"No altered files" : "Keine veränderten Dateien",
"Some files have not passed the integrity check. {link1} {link2}" : "Einige Dateien haben die Integritätsprüfung nicht bestanden. {link1} {link2}",
"List of invalid files…" : "Liste ungültiger Dateien …",
"Rescan…" : "Erneut scannen …",
"List of invalid files…" : "Liste ungültiger Dateien…",
"Rescan…" : "Erneut scannen…",
"Cron errors" : "Cron-Fehler",
"It was not possible to execute the cron job via CLI. The following technical errors have appeared:\n%s" : "Es war nicht möglich, den Cron-Job über die CLI auszuführen. Es sind folgende technische Fehler aufgetreten:\n%s",
"The last cron job ran without errors." : "Der letzte Cron-Job wurde ohne Fehler ausgeführt.",
@@ -465,19 +465,19 @@ OC.L10N.register(
"Failed to load groups" : "Gruppen konnten nicht geladen werden",
"Failed to create group" : "Gruppe konnte nicht erstellt werden",
"Groups" : "Gruppen",
"Creating group…" : "Erstelle Gruppe …",
"Creating group…" : "Erstelle Gruppe…",
"Create group" : "Gruppe erstellen",
"Group name" : "Gruppenname",
"Please enter a valid group name" : "Bitte einen gültigen Gruppennamen eingeben",
"Search groups…" : "Suche Gruppen …",
"Search groups…" : "Suche Gruppen…",
"List of groups. This list is not fully populated for performance reasons. The groups will be loaded as you navigate or search through the list." : "Liste der Gruppen. Diese Liste ist aus Leistungsgründen nicht vollständig gefüllt. Die Gruppen werden während des Navigierens oder Suchens geladen.",
"Loading groups…" : "Lade Gruppen …",
"Loading groups…" : "Lade Gruppen …",
"Could not load app discover section" : "Der App-Erkennungsabschnitt konnte nicht geladen werden",
"Could not render element" : "Element konnte nicht gerendert werden",
"Nothing to show" : "Nichts anzuzeigen",
"Could not load section content from app store." : "Abschnittsinhalt konnte nicht aus dem App Store geladen werden.",
"Loading" : "Lade",
"Fetching the latest news…" : "Aktuelle Nachrichten werden abgerufen …",
"Fetching the latest news…" : "Aktuelle Nachrichten werden abgerufen…",
"Carousel" : "Karussell",
"Previous slide" : "Vorherige Folie",
"Next slide" : "Nächste Folie",
@@ -985,15 +985,15 @@ OC.L10N.register(
"Exclude some groups from sharing" : "Bestimmte Gruppen vom Teilen ausschließen",
"Limit sharing to some groups" : "Teilen für bestimmte Gruppen erlauben",
"Also allow autocompletion on full match of the user id" : "Automatische Vervollständigung auch bei vollständiger Übereinstimmung der Benutzer-ID zulassen",
"Loading accounts …" : "Lade Konten …",
"Set account as admin for …" : "Konto als Administration setzen für …",
"_{userCount} account …_::_{userCount} accounts …_" : ["{userCount} Konto …","{userCount} Konten …"],
"Loading account …" : "Lade Konto …",
"Adding your device …" : "Dieses Gerät hinzufügen …",
"Sending…" : "Senden …",
"Loading accounts …" : "Lade Konten …",
"Set account as admin for …" : "Konto als Administration setzen für …",
"_{userCount} account …_::_{userCount} accounts …_" : ["{userCount} Konto …","{userCount} Konten …"],
"Loading account …" : "Lade Konto …",
"Adding your device …" : "Dieses Gerät hinzufügen …",
"Sending…" : "Senden…",
"Email sent" : "E-Mail gesendet",
"{progress}% Deploying …" : "{progress}% bereitstellen …",
"{progress}% Initializing …" : "{progress}% initialisiere …",
"{progress}% Deploying …" : "{progress}% bereitstellen …",
"{progress}% Initializing …" : "{progress}% initialisiere …",
"None/STARTTLS" : "Keine/STARTTLS",
"SSL" : "SSL",
"Credentials" : "Zugangsdaten",
+15 -15
View File
@@ -116,7 +116,7 @@
"Background jobs" : "Hintergrundaufgaben",
"Unlimited" : "Unbegrenzt",
"Verifying" : "Überprüfe",
"Verifying …" : "Überprüfe …",
"Verifying …" : "Überprüfe…",
"Verify" : "Überprüfen",
"Allowed admin IP ranges" : "Zulässige Administrations-IP-Bereiche",
"Admin IP filtering isn't applied." : "IP-Filterung durch Administration ist nicht aktiv.",
@@ -137,8 +137,8 @@
"Integrity checker has been disabled. Integrity cannot be verified." : "Die Integritätsprüfung wurde deaktiviert. Die Integrität kann nicht überprüft werden.",
"No altered files" : "Keine veränderten Dateien",
"Some files have not passed the integrity check. {link1} {link2}" : "Einige Dateien haben die Integritätsprüfung nicht bestanden. {link1} {link2}",
"List of invalid files…" : "Liste ungültiger Dateien …",
"Rescan…" : "Erneut scannen …",
"List of invalid files…" : "Liste ungültiger Dateien…",
"Rescan…" : "Erneut scannen…",
"Cron errors" : "Cron-Fehler",
"It was not possible to execute the cron job via CLI. The following technical errors have appeared:\n%s" : "Es war nicht möglich, den Cron-Job über die CLI auszuführen. Es sind folgende technische Fehler aufgetreten:\n%s",
"The last cron job ran without errors." : "Der letzte Cron-Job wurde ohne Fehler ausgeführt.",
@@ -463,19 +463,19 @@
"Failed to load groups" : "Gruppen konnten nicht geladen werden",
"Failed to create group" : "Gruppe konnte nicht erstellt werden",
"Groups" : "Gruppen",
"Creating group…" : "Erstelle Gruppe …",
"Creating group…" : "Erstelle Gruppe…",
"Create group" : "Gruppe erstellen",
"Group name" : "Gruppenname",
"Please enter a valid group name" : "Bitte einen gültigen Gruppennamen eingeben",
"Search groups…" : "Suche Gruppen …",
"Search groups…" : "Suche Gruppen…",
"List of groups. This list is not fully populated for performance reasons. The groups will be loaded as you navigate or search through the list." : "Liste der Gruppen. Diese Liste ist aus Leistungsgründen nicht vollständig gefüllt. Die Gruppen werden während des Navigierens oder Suchens geladen.",
"Loading groups…" : "Lade Gruppen …",
"Loading groups…" : "Lade Gruppen …",
"Could not load app discover section" : "Der App-Erkennungsabschnitt konnte nicht geladen werden",
"Could not render element" : "Element konnte nicht gerendert werden",
"Nothing to show" : "Nichts anzuzeigen",
"Could not load section content from app store." : "Abschnittsinhalt konnte nicht aus dem App Store geladen werden.",
"Loading" : "Lade",
"Fetching the latest news…" : "Aktuelle Nachrichten werden abgerufen …",
"Fetching the latest news…" : "Aktuelle Nachrichten werden abgerufen…",
"Carousel" : "Karussell",
"Previous slide" : "Vorherige Folie",
"Next slide" : "Nächste Folie",
@@ -983,15 +983,15 @@
"Exclude some groups from sharing" : "Bestimmte Gruppen vom Teilen ausschließen",
"Limit sharing to some groups" : "Teilen für bestimmte Gruppen erlauben",
"Also allow autocompletion on full match of the user id" : "Automatische Vervollständigung auch bei vollständiger Übereinstimmung der Benutzer-ID zulassen",
"Loading accounts …" : "Lade Konten …",
"Set account as admin for …" : "Konto als Administration setzen für …",
"_{userCount} account …_::_{userCount} accounts …_" : ["{userCount} Konto …","{userCount} Konten …"],
"Loading account …" : "Lade Konto …",
"Adding your device …" : "Dieses Gerät hinzufügen …",
"Sending…" : "Senden …",
"Loading accounts …" : "Lade Konten …",
"Set account as admin for …" : "Konto als Administration setzen für …",
"_{userCount} account …_::_{userCount} accounts …_" : ["{userCount} Konto …","{userCount} Konten …"],
"Loading account …" : "Lade Konto …",
"Adding your device …" : "Dieses Gerät hinzufügen …",
"Sending…" : "Senden…",
"Email sent" : "E-Mail gesendet",
"{progress}% Deploying …" : "{progress}% bereitstellen …",
"{progress}% Initializing …" : "{progress}% initialisiere …",
"{progress}% Deploying …" : "{progress}% bereitstellen …",
"{progress}% Initializing …" : "{progress}% initialisiere …",
"None/STARTTLS" : "Keine/STARTTLS",
"SSL" : "SSL",
"Credentials" : "Zugangsdaten",
-4
View File
@@ -305,8 +305,6 @@ OC.L10N.register(
"MariaDB version \"%1$s\" detected. MariaDB >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "La version MariaDB \"%1$s\" a été détectée. MariaDB >=%2$s et <=%3$s est suggérée pour de meilleures performances, stabilité et fonctionnalités avec cette version de Nextcloud.",
"MySQL version \"%1$s\" detected. MySQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "Version MySQL \"%1$s\" détectée. MySQL >=%2$s et <=%3$s est suggéré pour de meilleures performances, stabilité et fonctionnalités avec cette version de Nextcloud.",
"PostgreSQL version \"%1$s\" detected. PostgreSQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "PostgreSQL version \"%1$s\" détecté. PostgreSQL >=%2$s et <=%3$s est suggérée pour de meilleures performances, stabilité et fonctionnalités avec cette version de Nextcloud.",
"Nextcloud %d does not support your current version, so be sure to update the database before updating your Nextcloud Server." : "Nextcloud %d ne prend pas en charge votre version actuelle, assurez-vous de mettre à jour votre base de données avant de mettre à jour votre Serveur Nextcloud.",
"Oracle version \"%1$s\" detected. Oracle >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "Version d'Oracle \"%1$s\" détectée. Oracle >=%2$s and <=%3$s est suggérée pour de meilleures performances, stabilité et fonctionnalités avec cette version de Nextcloud.",
"SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend. This is particularly recommended when using the desktop client for file synchronisation. To migrate to another database use the command line tool: \"occ db:convert-type\"." : "SQLite est actuellement utilisé comme base de données principale. Pour les installations plus importantes, nous vous recommandons de changer de base de données. Cela est particulièrement recommandé lorsque vous utilisez le logiciel pour ordinateur de bureau pour la synchronisation des fichiers. Pour migrer vers une autre base de données, utilisez loutil en ligne de commande : « occ db:convert-type »",
"Unknown database platform" : "Plate-forme de base de données inconnue",
"Architecture" : "Architecture",
@@ -589,8 +587,6 @@ OC.L10N.register(
"Unable to update profile default setting" : "Impossible de mettre à jour les paramètres par défaut du profil",
"Profile" : "Profil",
"Enable or disable profile by default for new accounts." : "Active ou désactive le profil par défaut pour les nouveaux comptes.",
"Enable the profile picker" : "Activer le sélecteur de profils",
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Activer ou désactiver le sélecteur de profils dans le Sélecteur intelligent et dans les prévisualisations des liens de profil.",
"Password confirmation is required" : "Confirmation par mot de passe est requise",
"Failed to save setting" : "Échec de la sauvegarde des paramètres",
"{app}'s declarative setting field: {name}" : "champ de paramètre déclaratif de l'{app}: {name}",
-4
View File
@@ -303,8 +303,6 @@
"MariaDB version \"%1$s\" detected. MariaDB >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "La version MariaDB \"%1$s\" a été détectée. MariaDB >=%2$s et <=%3$s est suggérée pour de meilleures performances, stabilité et fonctionnalités avec cette version de Nextcloud.",
"MySQL version \"%1$s\" detected. MySQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "Version MySQL \"%1$s\" détectée. MySQL >=%2$s et <=%3$s est suggéré pour de meilleures performances, stabilité et fonctionnalités avec cette version de Nextcloud.",
"PostgreSQL version \"%1$s\" detected. PostgreSQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "PostgreSQL version \"%1$s\" détecté. PostgreSQL >=%2$s et <=%3$s est suggérée pour de meilleures performances, stabilité et fonctionnalités avec cette version de Nextcloud.",
"Nextcloud %d does not support your current version, so be sure to update the database before updating your Nextcloud Server." : "Nextcloud %d ne prend pas en charge votre version actuelle, assurez-vous de mettre à jour votre base de données avant de mettre à jour votre Serveur Nextcloud.",
"Oracle version \"%1$s\" detected. Oracle >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "Version d'Oracle \"%1$s\" détectée. Oracle >=%2$s and <=%3$s est suggérée pour de meilleures performances, stabilité et fonctionnalités avec cette version de Nextcloud.",
"SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend. This is particularly recommended when using the desktop client for file synchronisation. To migrate to another database use the command line tool: \"occ db:convert-type\"." : "SQLite est actuellement utilisé comme base de données principale. Pour les installations plus importantes, nous vous recommandons de changer de base de données. Cela est particulièrement recommandé lorsque vous utilisez le logiciel pour ordinateur de bureau pour la synchronisation des fichiers. Pour migrer vers une autre base de données, utilisez loutil en ligne de commande : « occ db:convert-type »",
"Unknown database platform" : "Plate-forme de base de données inconnue",
"Architecture" : "Architecture",
@@ -587,8 +585,6 @@
"Unable to update profile default setting" : "Impossible de mettre à jour les paramètres par défaut du profil",
"Profile" : "Profil",
"Enable or disable profile by default for new accounts." : "Active ou désactive le profil par défaut pour les nouveaux comptes.",
"Enable the profile picker" : "Activer le sélecteur de profils",
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Activer ou désactiver le sélecteur de profils dans le Sélecteur intelligent et dans les prévisualisations des liens de profil.",
"Password confirmation is required" : "Confirmation par mot de passe est requise",
"Failed to save setting" : "Échec de la sauvegarde des paramètres",
"{app}'s declarative setting field: {name}" : "champ de paramètre déclaratif de l'{app}: {name}",
-1
View File
@@ -285,7 +285,6 @@ OC.L10N.register(
"About" : "על אודות",
"Full name" : "שם מלא",
"Phone number" : "מספר טלפון",
"Role" : "תפקיד",
"Website" : "אתר",
"Locale" : "הגדרות אזוריות",
"Private" : "פרטי",
-1
View File
@@ -283,7 +283,6 @@
"About" : "על אודות",
"Full name" : "שם מלא",
"Phone number" : "מספר טלפון",
"Role" : "תפקיד",
"Website" : "אתר",
"Locale" : "הגדרות אזוריות",
"Private" : "פרטי",
+1 -1
View File
@@ -210,7 +210,7 @@ OC.L10N.register(
"_No scheduled tasks in the last day._::_No scheduled tasks in the last %n days._" : ["Geen geplande taken de afgelopen dag.","Geen geplande taken in de afgelopen %n dagen."],
"Temporary space available" : "Tijdelijke ruimte beschikbaar",
"Your database does not run with \"READ COMMITTED\" transaction isolation level. This can cause problems when multiple actions are executed in parallel." : "Je database draait niet met \"READ COMMITTED\" transactie-isolatie niveau. Dit kan problemen opleveren als er meerdere acties tegelijkertijd worden uitgevoerd.",
"Second factor configuration" : "Tweede factor configuratie",
"Second factor configuration" : "Twee factor configuratie",
"This instance has no second factor provider available." : "Deze instantie heeft geen twee factor provider beschikbaar.",
"Font file loading" : "Font bestand laden",
"Profile information" : "Profiel informatie",
+1 -1
View File
@@ -208,7 +208,7 @@
"_No scheduled tasks in the last day._::_No scheduled tasks in the last %n days._" : ["Geen geplande taken de afgelopen dag.","Geen geplande taken in de afgelopen %n dagen."],
"Temporary space available" : "Tijdelijke ruimte beschikbaar",
"Your database does not run with \"READ COMMITTED\" transaction isolation level. This can cause problems when multiple actions are executed in parallel." : "Je database draait niet met \"READ COMMITTED\" transactie-isolatie niveau. Dit kan problemen opleveren als er meerdere acties tegelijkertijd worden uitgevoerd.",
"Second factor configuration" : "Tweede factor configuratie",
"Second factor configuration" : "Twee factor configuratie",
"This instance has no second factor provider available." : "Deze instantie heeft geen twee factor provider beschikbaar.",
"Font file loading" : "Font bestand laden",
"Profile information" : "Profiel informatie",
-1
View File
@@ -306,7 +306,6 @@ OC.L10N.register(
"MySQL version \"%1$s\" detected. MySQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "Виявлено версію MySQL \"%1$s\". Рекомендується використовувати MySQL >=%2$s та <=%3$s для кращої продуктивності, стабільності та функціональності з цією версією Nextcloud.",
"PostgreSQL version \"%1$s\" detected. PostgreSQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "Виявлено версію PostgreSQL \"%1$s\". Рекомендується використовувати PostgreSQL >=%2$s та <=%3$s для кращої продуктивності, стабільності та функціональності з цією версією Nextcloud.",
"Nextcloud %d does not support your current version, so be sure to update the database before updating your Nextcloud Server." : "Nextcloud %d не підтримує вашу поточну версію. Потрібно оновити вашу базу даних, перш ніж оновлювати сервер Nextcloud.",
"Oracle version \"%1$s\" detected. Oracle >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "Виявлено версію Oracle \"%1$s\". Для кращої продуктивності, стабільності та функціональності цієї версії Nextcloud рекомендується використовувати Oracle >=%2$s та <=%3$s.",
"SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend. This is particularly recommended when using the desktop client for file synchronisation. To migrate to another database use the command line tool: \"occ db:convert-type\"." : "Наразі SQLite використовується як база даних. Для більш продуктивних примірників рекомендується переключитися на іншу базу даних. Зокрема це рекомендується у разі використання клієнтів синхронізації файлів для робочих станцій. Щоб мігрувати до іншої бази даних, використовуйте інструмент командного рядка: \"occ db:convert-type\".",
"Unknown database platform" : "Невідома платформа бази даних",
"Architecture" : "Архітектура",
-1
View File
@@ -304,7 +304,6 @@
"MySQL version \"%1$s\" detected. MySQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "Виявлено версію MySQL \"%1$s\". Рекомендується використовувати MySQL >=%2$s та <=%3$s для кращої продуктивності, стабільності та функціональності з цією версією Nextcloud.",
"PostgreSQL version \"%1$s\" detected. PostgreSQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "Виявлено версію PostgreSQL \"%1$s\". Рекомендується використовувати PostgreSQL >=%2$s та <=%3$s для кращої продуктивності, стабільності та функціональності з цією версією Nextcloud.",
"Nextcloud %d does not support your current version, so be sure to update the database before updating your Nextcloud Server." : "Nextcloud %d не підтримує вашу поточну версію. Потрібно оновити вашу базу даних, перш ніж оновлювати сервер Nextcloud.",
"Oracle version \"%1$s\" detected. Oracle >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud." : "Виявлено версію Oracle \"%1$s\". Для кращої продуктивності, стабільності та функціональності цієї версії Nextcloud рекомендується використовувати Oracle >=%2$s та <=%3$s.",
"SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend. This is particularly recommended when using the desktop client for file synchronisation. To migrate to another database use the command line tool: \"occ db:convert-type\"." : "Наразі SQLite використовується як база даних. Для більш продуктивних примірників рекомендується переключитися на іншу базу даних. Зокрема це рекомендується у разі використання клієнтів синхронізації файлів для робочих станцій. Щоб мігрувати до іншої бази даних, використовуйте інструмент командного рядка: \"occ db:convert-type\".",
"Unknown database platform" : "Невідома платформа бази даних",
"Architecture" : "Архітектура",
+6 -26
View File
@@ -108,8 +108,8 @@ OC.L10N.register(
"Artificial Intelligence" : "人工智能",
"None / STARTTLS" : "无 / STARTTLS",
"Email server" : "电子邮件服务器",
"Mail Providers" : "邮件提供",
"Mail provider enables sending emails directly through the user's personal email account. At present, this functionality is limited to calendar invitations. It requires Nextcloud Mail 4.1 and an email account in Nextcloud Mail that matches the user's email address in Nextcloud." : "邮件提供允许直接通过用户的个人电子邮件账号发送电子邮件。目前,此功能仅限于日历邀请。它需要 Nextcloud Mail 4.1 和 Nextcloud Mail 中与用户在 Nextcloud 中的电子邮件地址匹配的电子邮件账号。",
"Mail Providers" : "邮件提供",
"Mail provider enables sending emails directly through the user's personal email account. At present, this functionality is limited to calendar invitations. It requires Nextcloud Mail 4.1 and an email account in Nextcloud Mail that matches the user's email address in Nextcloud." : "邮件提供允许直接通过用户的个人电子邮件帐户发送电子邮件。目前,此功能仅限于日历邀请。它需要 Nextcloud Mail 4.1 和 Nextcloud Mail 中与用户在 Nextcloud 中的电子邮件地址匹配的电子邮件帐户。",
"Send emails using" : "使用 email 发送",
"User's email account" : "用户的 email 账号",
"System email account" : "系统 email 账号",
@@ -220,8 +220,8 @@ OC.L10N.register(
"MySQL Unicode support" : "MySQL Unicode 支持",
"MySQL is used as database and does support 4-byte characters" : "MySQL用作数据库并且支持4字节字符",
"MySQL is used as database but does not support 4-byte characters. To be able to handle 4-byte characters (like emojis) without issues in filenames or comments for example it is recommended to enable the 4-byte support in MySQL." : "MySQL 用作数据库,但不支持 4 字节字符。为了能够处理 4 字节字符(如表情符号),而不会在文件名或注释中出现问题,建议在 MySQL 中启用 4 字节支持。",
"OCS provider resolving" : "OCS 提供解析",
"Could not check if your web server properly resolves the OCM and OCS provider URLs." : "无法检查您的 Web 服务器是否正确解析 OCM 和 OCS 提供 URL。",
"OCS provider resolving" : "OCS提供解析",
"Could not check if your web server properly resolves the OCM and OCS provider URLs." : "无法检查您的 Web 服务器是否正确解析 OCM 和 OCS 提供 URL。",
"Your web server is not properly set up to resolve %1$s.\nThis is most likely related to a web server configuration that was not updated to deliver this folder directly.\nPlease compare your configuration against the shipped rewrite rules in \".htaccess\" for Apache or the provided one in the documentation for Nginx.\nOn Nginx those are typically the lines starting with \"location ~\" that need an update." : "您的 Web 服务器未正确设置以解析 %1$s。\n这很可能与未更新以直接提供此文件夹的 Web 服务器配置有关。\n请将您的配置与 Apache 的 \".htaccess\" 中提供的重写规则或 Nginx 文档中提供的重写规则进行比较。\n在 Nginx 上,通常以 \"location ~\" 开头的行需要更新。",
"Overwrite CLI URL" : "覆盖 CLI URL",
"The \"overwrite.cli.url\" option in your config.php is correctly set to \"%s\"." : "您的 config.php 中的 \"overwrite.cli.url\" 选项正确设置为 \"%s\" 。",
@@ -330,10 +330,6 @@ OC.L10N.register(
"Database transaction isolation level" : "数据库事务隔离级别",
"Your database does not run with \"READ COMMITTED\" transaction isolation level. This can cause problems when multiple actions are executed in parallel." : "数据库没有运行在“READ COMMITTED”事务隔离级别。当多项操作同时执行时将产生问题。",
"Was not able to get transaction isolation level: %s" : "未能获取事务隔离级别:%s",
"Second factor configuration" : "第二因素配置",
"This instance has no second factor provider available." : "此实例没有可用的第二因素提供者。",
"Second factor providers are available but two-factor authentication is not enforced." : "第二因素提供者可用,但未强制执行双因素身份验证。",
"Second factor providers are available and enforced: %s." : "第二个因素提供者可用并强制执行:%s。",
".well-known URLs" : ".well-known URL",
"`check_for_working_wellknown_setup` is set to false in your configuration, so this check was skipped." : "您的配置中的 `check_for_working_wellknown_setup` 设置为 false,因此跳过了此检查。",
"Could not check that your web server serves `.well-known` correctly. Please check manually." : "无法检查您的网络服务器是否正确服务 `.well-known`。 请手动检查。",
@@ -419,7 +415,7 @@ OC.L10N.register(
"This text will be shown on the public link upload page when the file list is hidden." : "这些内容将在公开链接上传页中当文件列表隐藏时显示。",
"Default share permissions" : "默认共享权限",
"Two-Factor Authentication" : "两步验证",
"Two-factor authentication can be enforced for all accounts and specific groups. If they do not have a two-factor provider configured, they will be unable to log into the system." : "可以对所有账号和特定组强制执行双因素身份验证。如果他们没有配置双因素提供,他们将无法登录系统。",
"Two-factor authentication can be enforced for all accounts and specific groups. If they do not have a two-factor provider configured, they will be unable to log into the system." : "可以对所有帐户和特定组强制执行两步验证。 如果他们没有配置两步验证提供,他们将无法登录系统。",
"Enforce two-factor authentication" : "强制启用两步验证",
"Limit to groups" : "限制于组",
"Enforcement of two-factor authentication can be set for certain groups only." : "可以仅为某些组设置两步验证的强制执行。",
@@ -442,16 +438,12 @@ OC.L10N.register(
"This app is supported via your current Nextcloud subscription." : "根据您的 Nextcloud 订阅,此应用受到支持。",
"Featured apps are developed by and within the community. They offer central functionality and are ready for production use." : "特色应用由社区并在社区内开发。 它们提供了中心功能,并准备投入生产使用。",
"Community rating: {score}/5" : "社区评分:{score}/5",
"Office suite switching is managed through the Nextcloud All-in-One interface." : "办公套件的切换是通过 Nextcloud All-in-One 界面进行管理的。",
"Please use the AIO interface to switch between office suites." : "请使用 AIO 界面在办公套件之间切换。",
"Select your preferred office suite. Please note that installing requires manual server setup." : "选择您的首选办公套件。请注意,安装需要手动设置服务器。",
"installed" : "已安装",
"Learn more" : "了解更多",
"Disable office suites" : "禁用办公套件",
"Disable all" : "禁用全部",
"Download and enable all" : "下载并启用全部",
"All office suites disabled" : "所有办公套件已禁用",
"{name} enabled" : "{name} 已启用",
"All apps are up-to-date." : "所有的应用程序都是最新的。",
"Icon" : "图标",
"Name" : "名称",
@@ -593,11 +585,8 @@ OC.L10N.register(
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 accounts depending on the usage)." : "cron.php 注册一个 webcron 服务,使用 HTTP 每 5 分钟执行一次 cron.php 文件。使用场景:非常小的实例(1-5 个账号,具体取决于使用用量)",
"Cron (Recommended)" : "Cron(推荐)",
"Unable to update profile default setting" : "无法更新个人资料默认设置",
"Unable to update profile picker setting" : "无法更新个人资料选择器设置",
"Profile" : "个人资料",
"Enable or disable profile by default for new accounts." : "默认情况下为新账号启用或禁用配置文件。",
"Enable the profile picker" : "启用个人资料选择器",
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "在智能选择器和个人资料链接预览中启用或禁用个人资料选择器。",
"Password confirmation is required" : "需要密码确认",
"Failed to save setting" : "保存设置失败",
"{app}'s declarative setting field: {name}" : "{app} 的声明性设置字段:{name}",
@@ -914,17 +903,8 @@ OC.L10N.register(
"App bundles" : "应用捆绑包",
"Featured apps" : "精选应用",
"Supported apps" : "支持的应用",
"Best Nextcloud integration" : "最佳 Nextcloud 集成",
"Best Nextcloud integration" : "最佳Nextcloud整合",
"Open source" : "开源",
"Good performance" : "性能良好",
"Best security: documents never leave your server" : "最佳安全性:文档永远不会离开您的服务器",
"Best ODF compatibility" : "最佳 ODF 兼容性",
"Best support for legacy files" : "对旧版文件的最佳支持",
"Good Nextcloud integration" : "Nextcloud 集成良好",
"Open core" : "开放核心",
"Best performance" : "最佳性能",
"Limited ODF compatibility" : "ODF 兼容性有限",
"Best Microsoft compatibility" : "最佳 Microsoft 兼容性",
"Show to everyone" : "显示给所有人",
"Show to logged in accounts only" : "仅向已登录账号显示",
"Hide" : "隐藏",
+6 -26
View File
@@ -106,8 +106,8 @@
"Artificial Intelligence" : "人工智能",
"None / STARTTLS" : "无 / STARTTLS",
"Email server" : "电子邮件服务器",
"Mail Providers" : "邮件提供",
"Mail provider enables sending emails directly through the user's personal email account. At present, this functionality is limited to calendar invitations. It requires Nextcloud Mail 4.1 and an email account in Nextcloud Mail that matches the user's email address in Nextcloud." : "邮件提供允许直接通过用户的个人电子邮件账号发送电子邮件。目前,此功能仅限于日历邀请。它需要 Nextcloud Mail 4.1 和 Nextcloud Mail 中与用户在 Nextcloud 中的电子邮件地址匹配的电子邮件账号。",
"Mail Providers" : "邮件提供",
"Mail provider enables sending emails directly through the user's personal email account. At present, this functionality is limited to calendar invitations. It requires Nextcloud Mail 4.1 and an email account in Nextcloud Mail that matches the user's email address in Nextcloud." : "邮件提供允许直接通过用户的个人电子邮件帐户发送电子邮件。目前,此功能仅限于日历邀请。它需要 Nextcloud Mail 4.1 和 Nextcloud Mail 中与用户在 Nextcloud 中的电子邮件地址匹配的电子邮件帐户。",
"Send emails using" : "使用 email 发送",
"User's email account" : "用户的 email 账号",
"System email account" : "系统 email 账号",
@@ -218,8 +218,8 @@
"MySQL Unicode support" : "MySQL Unicode 支持",
"MySQL is used as database and does support 4-byte characters" : "MySQL用作数据库并且支持4字节字符",
"MySQL is used as database but does not support 4-byte characters. To be able to handle 4-byte characters (like emojis) without issues in filenames or comments for example it is recommended to enable the 4-byte support in MySQL." : "MySQL 用作数据库,但不支持 4 字节字符。为了能够处理 4 字节字符(如表情符号),而不会在文件名或注释中出现问题,建议在 MySQL 中启用 4 字节支持。",
"OCS provider resolving" : "OCS 提供解析",
"Could not check if your web server properly resolves the OCM and OCS provider URLs." : "无法检查您的 Web 服务器是否正确解析 OCM 和 OCS 提供 URL。",
"OCS provider resolving" : "OCS提供解析",
"Could not check if your web server properly resolves the OCM and OCS provider URLs." : "无法检查您的 Web 服务器是否正确解析 OCM 和 OCS 提供 URL。",
"Your web server is not properly set up to resolve %1$s.\nThis is most likely related to a web server configuration that was not updated to deliver this folder directly.\nPlease compare your configuration against the shipped rewrite rules in \".htaccess\" for Apache or the provided one in the documentation for Nginx.\nOn Nginx those are typically the lines starting with \"location ~\" that need an update." : "您的 Web 服务器未正确设置以解析 %1$s。\n这很可能与未更新以直接提供此文件夹的 Web 服务器配置有关。\n请将您的配置与 Apache 的 \".htaccess\" 中提供的重写规则或 Nginx 文档中提供的重写规则进行比较。\n在 Nginx 上,通常以 \"location ~\" 开头的行需要更新。",
"Overwrite CLI URL" : "覆盖 CLI URL",
"The \"overwrite.cli.url\" option in your config.php is correctly set to \"%s\"." : "您的 config.php 中的 \"overwrite.cli.url\" 选项正确设置为 \"%s\" 。",
@@ -328,10 +328,6 @@
"Database transaction isolation level" : "数据库事务隔离级别",
"Your database does not run with \"READ COMMITTED\" transaction isolation level. This can cause problems when multiple actions are executed in parallel." : "数据库没有运行在“READ COMMITTED”事务隔离级别。当多项操作同时执行时将产生问题。",
"Was not able to get transaction isolation level: %s" : "未能获取事务隔离级别:%s",
"Second factor configuration" : "第二因素配置",
"This instance has no second factor provider available." : "此实例没有可用的第二因素提供者。",
"Second factor providers are available but two-factor authentication is not enforced." : "第二因素提供者可用,但未强制执行双因素身份验证。",
"Second factor providers are available and enforced: %s." : "第二个因素提供者可用并强制执行:%s。",
".well-known URLs" : ".well-known URL",
"`check_for_working_wellknown_setup` is set to false in your configuration, so this check was skipped." : "您的配置中的 `check_for_working_wellknown_setup` 设置为 false,因此跳过了此检查。",
"Could not check that your web server serves `.well-known` correctly. Please check manually." : "无法检查您的网络服务器是否正确服务 `.well-known`。 请手动检查。",
@@ -417,7 +413,7 @@
"This text will be shown on the public link upload page when the file list is hidden." : "这些内容将在公开链接上传页中当文件列表隐藏时显示。",
"Default share permissions" : "默认共享权限",
"Two-Factor Authentication" : "两步验证",
"Two-factor authentication can be enforced for all accounts and specific groups. If they do not have a two-factor provider configured, they will be unable to log into the system." : "可以对所有账号和特定组强制执行双因素身份验证。如果他们没有配置双因素提供,他们将无法登录系统。",
"Two-factor authentication can be enforced for all accounts and specific groups. If they do not have a two-factor provider configured, they will be unable to log into the system." : "可以对所有帐户和特定组强制执行两步验证。 如果他们没有配置两步验证提供,他们将无法登录系统。",
"Enforce two-factor authentication" : "强制启用两步验证",
"Limit to groups" : "限制于组",
"Enforcement of two-factor authentication can be set for certain groups only." : "可以仅为某些组设置两步验证的强制执行。",
@@ -440,16 +436,12 @@
"This app is supported via your current Nextcloud subscription." : "根据您的 Nextcloud 订阅,此应用受到支持。",
"Featured apps are developed by and within the community. They offer central functionality and are ready for production use." : "特色应用由社区并在社区内开发。 它们提供了中心功能,并准备投入生产使用。",
"Community rating: {score}/5" : "社区评分:{score}/5",
"Office suite switching is managed through the Nextcloud All-in-One interface." : "办公套件的切换是通过 Nextcloud All-in-One 界面进行管理的。",
"Please use the AIO interface to switch between office suites." : "请使用 AIO 界面在办公套件之间切换。",
"Select your preferred office suite. Please note that installing requires manual server setup." : "选择您的首选办公套件。请注意,安装需要手动设置服务器。",
"installed" : "已安装",
"Learn more" : "了解更多",
"Disable office suites" : "禁用办公套件",
"Disable all" : "禁用全部",
"Download and enable all" : "下载并启用全部",
"All office suites disabled" : "所有办公套件已禁用",
"{name} enabled" : "{name} 已启用",
"All apps are up-to-date." : "所有的应用程序都是最新的。",
"Icon" : "图标",
"Name" : "名称",
@@ -591,11 +583,8 @@
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 accounts depending on the usage)." : "cron.php 注册一个 webcron 服务,使用 HTTP 每 5 分钟执行一次 cron.php 文件。使用场景:非常小的实例(1-5 个账号,具体取决于使用用量)",
"Cron (Recommended)" : "Cron(推荐)",
"Unable to update profile default setting" : "无法更新个人资料默认设置",
"Unable to update profile picker setting" : "无法更新个人资料选择器设置",
"Profile" : "个人资料",
"Enable or disable profile by default for new accounts." : "默认情况下为新账号启用或禁用配置文件。",
"Enable the profile picker" : "启用个人资料选择器",
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "在智能选择器和个人资料链接预览中启用或禁用个人资料选择器。",
"Password confirmation is required" : "需要密码确认",
"Failed to save setting" : "保存设置失败",
"{app}'s declarative setting field: {name}" : "{app} 的声明性设置字段:{name}",
@@ -912,17 +901,8 @@
"App bundles" : "应用捆绑包",
"Featured apps" : "精选应用",
"Supported apps" : "支持的应用",
"Best Nextcloud integration" : "最佳 Nextcloud 集成",
"Best Nextcloud integration" : "最佳Nextcloud整合",
"Open source" : "开源",
"Good performance" : "性能良好",
"Best security: documents never leave your server" : "最佳安全性:文档永远不会离开您的服务器",
"Best ODF compatibility" : "最佳 ODF 兼容性",
"Best support for legacy files" : "对旧版文件的最佳支持",
"Good Nextcloud integration" : "Nextcloud 集成良好",
"Open core" : "开放核心",
"Best performance" : "最佳性能",
"Limited ODF compatibility" : "ODF 兼容性有限",
"Best Microsoft compatibility" : "最佳 Microsoft 兼容性",
"Show to everyone" : "显示给所有人",
"Show to logged in accounts only" : "仅向已登录账号显示",
"Hide" : "隐藏",
+1 -1
View File
@@ -35,7 +35,7 @@ class ConfigLexicon implements ILexicon {
public function getAppConfigs(): array {
return [
new Entry(key: self::LOGIN_QRCODE_ONETIME, type: ValueType::BOOL, defaultRaw: true, definition: 'Use onetime QR codes for app passwords', note: 'Limits compatibility for mobile apps to versions released in 2026 or later'),
new Entry(key: self::LOGIN_QRCODE_ONETIME, type: ValueType::BOOL, defaultRaw: false, definition: 'Use onetime QR codes for app passwords', note: 'Limits compatibility for mobile apps to versions released in 2026 or later'),
];
}
@@ -7,7 +7,7 @@
*/
namespace OCA\Settings\Controller;
use OCA\Settings\Settings\Admin\Mail;
use OCA\Settings\Settings\Admin\Overview;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
@@ -48,7 +48,7 @@ class MailSettingsController extends Controller {
/**
* Sets the email settings
*/
#[AuthorizedAdminSetting(settings: Mail::class)]
#[AuthorizedAdminSetting(settings: Overview::class)]
#[PasswordConfirmationRequired]
public function setMailSettings(
string $mail_domain,
@@ -102,7 +102,7 @@ class MailSettingsController extends Controller {
/**
* Store the credentials used for SMTP in the config
*/
#[AuthorizedAdminSetting(settings: Mail::class)]
#[AuthorizedAdminSetting(settings: Overview::class)]
#[PasswordConfirmationRequired]
public function storeCredentials(string $mail_smtpname, ?string $mail_smtppassword): DataResponse {
if ($mail_smtppassword === '********') {
@@ -122,7 +122,7 @@ class MailSettingsController extends Controller {
* Send a mail to test the settings
* @return DataResponse
*/
#[AuthorizedAdminSetting(settings: Mail::class)]
#[AuthorizedAdminSetting(settings: Overview::class)]
public function sendTestMail() {
$email = $this->config->getUserValue($this->userSession->getUser()->getUID(), $this->appName, 'email', '');
if (!empty($email)) {
@@ -14,7 +14,6 @@ use OC\AppFramework\Http;
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Middleware;
use OCP\Group\ISubAdmin;
@@ -45,7 +44,7 @@ class SubadminMiddleware extends Middleware {
#[Override]
public function beforeController(Controller $controller, string $methodName): void {
if (!$this->reflector->hasAnnotation('NoSubAdminRequired') && !$this->reflector->hasAnnotationOrAttribute('AuthorizedAdminSetting', AuthorizedAdminSetting::class)) {
if (!$this->reflector->hasAnnotation('NoSubAdminRequired') && !$this->reflector->hasAnnotation('AuthorizedAdminSetting')) {
if (!$this->isSubAdmin()) {
throw new NotAdminException($this->l10n->t('Logged in account must be a sub admin'));
}
@@ -1,55 +0,0 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { describe, expect, it } from 'vitest'
import { detect } from '../utils/userAgentDetect.ts'
describe('Android Chrome detection', () => {
it('modern Android Chrome (no Build/ string, post-2021) should match androidChrome', () => {
const ua = 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Mobile Safari/537.36'
expect(detect(ua)).toEqual({
id: 'androidChrome',
version: '132',
})
})
it('legacy Android Chrome (with Build/ string, pre-2021) should match androidChrome', () => {
const ua = 'Mozilla/5.0 (Linux; Android 10; SM-G973F Build/QP1A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36'
expect(detect(ua)).toEqual({
id: 'androidChrome',
version: '130',
})
})
it('Android Chrome on tablet (no "Mobile" in UA) should match androidChrome', () => {
const ua = 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
expect(detect(ua)).toEqual({
id: 'androidChrome',
version: '131',
})
})
})
describe('Desktop Chrome regression tests', () => {
it('Desktop Chrome on Linux should still match chrome', () => {
const ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36'
expect(detect(ua)).toEqual({
id: 'chrome',
version: '132',
os: 'Linux',
})
})
})
describe('Desktop Firefox regression tests', () => {
it('Desktop Firefox on Linux should still match firefox', () => {
const ua = 'Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0'
expect(detect(ua)).toEqual({
id: 'firefox',
version: '124',
os: 'Linux',
})
})
})
+40 -2
View File
@@ -100,8 +100,35 @@ import NcDateTime from '@nextcloud/vue/components/NcDateTime'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import { TokenType, useAuthTokenStore } from '../store/authtoken.ts'
import { detect } from '../utils/userAgentDetect.ts'
// When using capture groups the following parts are extracted the first is used as the version number, the second as the OS
const userAgentMap = {
ie: /(?:MSIE|Trident|Trident\/7.0; rv)[ :](\d+)/,
// Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
edge: /^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (?:Mobile Safari|Safari)\/[0-9.]+ Edge\/[0-9.]+$/,
// Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference
firefox: /^Mozilla\/5\.0 \([^)]*(Windows|OS X|Linux)[^)]+\) Gecko\/[0-9.]+ Firefox\/(\d+)(?:\.\d)?$/,
// Chrome User Agent from https://developer.chrome.com/multidevice/user-agent
chrome: /^Mozilla\/5\.0 \([^)]*(Windows|OS X|Linux)[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/(\d+)[0-9.]+ (?:Mobile Safari|Safari)\/[0-9.]+$/,
// Safari User Agent from http://www.useragentstring.com/pages/Safari/
safari: /^Mozilla\/5\.0 \([^)]*(Windows|OS X)[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)(?: Version\/([0-9]+)[0-9.]+)? Safari\/[0-9.A-Z]+$/,
// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
androidChrome: /Android.*(?:; (.*) Build\/).*Chrome\/(\d+)[0-9.]+/,
iphone: / *CPU +iPhone +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
ipad: /\(iPad; *CPU +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
iosClient: /^Mozilla\/5\.0 \(iOS\) (?:ownCloud|Nextcloud)-iOS.*$/,
androidClient: /^Mozilla\/5\.0 \(Android\) (?:ownCloud|Nextcloud)-android.*$/,
iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud-Talk.*$/,
androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud-Talk.*$/,
// DAVx5/3.3.8-beta2-gplay (2021/01/02; dav4jvm; okhttp/4.9.0) Android/10
davx5: /DAV(?:droid|x5)\/([^ ]+)/,
// Mozilla/5.0 (U; Linux; Maemo; Jolla; Sailfish; like Android 4.3) AppleWebKit/538.1 (KHTML, like Gecko) WebPirate/2.0 like Mobile Safari/538.1 (compatible)
webPirate: /(Sailfish).*WebPirate\/(\d+)/,
// Mozilla/5.0 (Maemo; Linux; U; Jolla; Sailfish; Mobile; rv:31.0) Gecko/31.0 Firefox/31.0 SailfishBrowser/1.0
sailfishBrowser: /(Sailfish).*SailfishBrowser\/(\d+)/,
// Neon 1.0.0+1
neon: /Neon \d+\.\d+\.\d+\+\d+/,
}
const nameMap = {
edge: 'Microsoft Edge',
firefox: 'Firefox',
@@ -176,7 +203,18 @@ export default defineComponent({
}
}
return detect(this.token.name)
for (const client in userAgentMap) {
const matches = this.token.name.match(userAgentMap[client])
if (matches) {
return {
id: client,
os: matches[2] && matches[1],
version: matches[2] ?? matches[1],
}
}
}
return null
},
/**
@@ -1,33 +0,0 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { userAgentMap } from './userAgentMap.ts'
export interface DetectedUserAgent {
id: string
version?: string
os?: string
}
/**
* Detect the client from a user agent string.
*
* @param ua Raw user agent string
* @return Detected client information or null if unknown
*/
export function detect(ua: string): DetectedUserAgent | null {
for (const id in userAgentMap) {
const matches = ua.match(userAgentMap[id])
if (matches) {
return {
id,
version: matches[2] ?? matches[1],
os: matches[2] && matches[1],
}
}
}
return null
}
-35
View File
@@ -1,35 +0,0 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
// When using capture groups the following parts are extracted
// the first is used as the version number, the second as the OS
// Exception: single-group regexes (ie, androidChrome) use the first group as the version.
export const userAgentMap = {
ie: /(?:MSIE|Trident|Trident\/7.0; rv)[ :](\d+)/,
// Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
edge: /^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (?:Mobile Safari|Safari)\/[0-9.]+ Edge\/[0-9.]+$/,
// Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference
firefox: /^Mozilla\/5\.0 \((?![^)]*Android)[^)]*(Windows|OS X|Linux)[^)]+\) Gecko\/[0-9.]+ Firefox\/(\d+)(?:\.\d)?$/,
// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
androidChrome: /^Mozilla\/5\.0 \(Linux; Android[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/(\d+)[0-9.]+ (?:Mobile )?Safari\/[0-9.]+$/,
// Chrome User Agent from https://developer.chrome.com/multidevice/user-agent
chrome: /^Mozilla\/5\.0 \((?![^)]*Android)[^)]*(Windows|OS X|Linux)[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/(\d+)[0-9.]+ (?:Mobile Safari|Safari)\/[0-9.]+$/,
// Safari User Agent from http://www.useragentstring.com/pages/Safari/
safari: /^Mozilla\/5\.0 \([^)]*(Windows|OS X)[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)(?: Version\/([0-9]+)[0-9.]+)? Safari\/[0-9.A-Z]+$/,
iphone: / *CPU +iPhone +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
ipad: /\(iPad; *CPU +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
iosClient: /^Mozilla\/5\.0 \(iOS\) (?:ownCloud|Nextcloud)-iOS.*$/,
androidClient: /^Mozilla\/5\.0 \(Android\) (?:ownCloud|Nextcloud)-android.*$/,
iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud-Talk.*$/,
androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud-Talk.*$/,
// DAVx5/3.3.8-beta2-gplay (2021/01/02; dav4jvm; okhttp/4.9.0) Android/10
davx5: /DAV(?:droid|x5)\/([^ ]+)/,
// Mozilla/5.0 (U; Linux; Maemo; Jolla; Sailfish; like Android 4.3) AppleWebKit/538.1 (KHTML, like Gecko) WebPirate/2.0 like Mobile Safari/538.1 (compatible)
webPirate: /(Sailfish).*WebPirate\/(\d+)/,
// Mozilla/5.0 (Maemo; Linux; U; Jolla; Sailfish; Mobile; rv:31.0) Gecko/31.0 Firefox/31.0 SailfishBrowser/1.0
sailfishBrowser: /(Sailfish).*SailfishBrowser\/(\d+)/,
// Neon 1.0.0+1
neon: /Neon \d+\.\d+\.\d+\+\d+/,
}
@@ -14,7 +14,6 @@ use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OCA\Settings\Middleware\SubadminMiddleware;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Group\ISubAdmin;
use OCP\IL10N;
@@ -63,16 +62,11 @@ class SubadminMiddlewareTest extends \Test\TestCase {
$this->expectException(NotAdminException::class);
$this->reflector
->expects($this->exactly(1))
->expects($this->exactly(2))
->method('hasAnnotation')
->willReturnMap([
['NoSubAdminRequired', false],
]);
$this->reflector
->expects($this->exactly(1))
->method('hasAnnotationOrAttribute')
->willReturnMap([
['AuthorizedAdminSetting', AuthorizedAdminSetting::class, false],
['AuthorizedAdminSetting', false],
]);
$this->subAdminManager
@@ -100,16 +94,11 @@ class SubadminMiddlewareTest extends \Test\TestCase {
public function testBeforeControllerAsSubAdminWithoutAnnotation(): void {
$this->reflector
->expects($this->exactly(1))
->expects($this->exactly(2))
->method('hasAnnotation')
->willReturnMap([
['NoSubAdminRequired', false],
]);
$this->reflector
->expects($this->exactly(1))
->method('hasAnnotationOrAttribute')
->willReturnMap([
['AuthorizedAdminSetting', AuthorizedAdminSetting::class, false],
['AuthorizedAdminSetting', false],
]);
$this->subAdminManager
+5 -5
View File
@@ -40,7 +40,7 @@ OC.L10N.register(
"<strong>System tags</strong> for a file have been modified" : "<strong>System-Tags</strong> für eine Datei wurden geändert",
"Files" : "Dateien",
"Tags" : "Tags",
"All tagged %s …" : "Alle Schlagworte %s hinzugefügt …",
"All tagged %s …" : "Alle Schlagworte %s hinzugefügt …",
"tagged %s" : "Schlagwort %s hinzugefügt",
"Collaborative tags" : "Kollaborative Schlagworte",
"Collaborative tagging functionality which shares tags among people." : "Gemeinschaftliche Schlagwort-Funktionalität, welche Schlagworte unter den Benutzern teilt.",
@@ -75,7 +75,7 @@ OC.L10N.register(
"Only admins can create new tags" : "Nur die Administration kann neue Schlagworte erstellen",
"Failed to apply tags changes" : "Schlagwort-Änderungen konnten nicht angewendet werden",
"Manage tags" : "Schlagworte verwalten",
"Applying tags changes…" : "Schlagwort-Änderungen werden angewendet …",
"Applying tags changes…" : "Schlagwort-Änderungen werden angewendet…",
"Search or create tag" : "Schlagwort suchen oder erstellen",
"Search tag" : "Schlagworte suchen",
"Change tag color" : "Schlagwortfarbe ändern",
@@ -110,8 +110,8 @@ OC.L10N.register(
"Failed to load tags for file" : "Schlagworte für Datei konnten nicht geladen werden",
"Failed to set tag for file" : "Schlagwort für Datei konnte nicht gesetzt werden",
"Failed to delete tag for file" : "Schlagwort für Datei konnte nicht gelöscht werden",
"Collaborative tags …" : "Kollaborative Schlagworte …",
"Loading …" : "Lade …",
"Loading collaborative tags …" : "Kollaborative Schlagworte laden …"
"Collaborative tags …" : "Kollaborative Schlagworte …",
"Loading …" : "Lade …",
"Loading collaborative tags …" : "Kollaborative Schlagworte laden …"
},
"nplurals=2; plural=(n != 1);");
+5 -5
View File
@@ -38,7 +38,7 @@
"<strong>System tags</strong> for a file have been modified" : "<strong>System-Tags</strong> für eine Datei wurden geändert",
"Files" : "Dateien",
"Tags" : "Tags",
"All tagged %s …" : "Alle Schlagworte %s hinzugefügt …",
"All tagged %s …" : "Alle Schlagworte %s hinzugefügt …",
"tagged %s" : "Schlagwort %s hinzugefügt",
"Collaborative tags" : "Kollaborative Schlagworte",
"Collaborative tagging functionality which shares tags among people." : "Gemeinschaftliche Schlagwort-Funktionalität, welche Schlagworte unter den Benutzern teilt.",
@@ -73,7 +73,7 @@
"Only admins can create new tags" : "Nur die Administration kann neue Schlagworte erstellen",
"Failed to apply tags changes" : "Schlagwort-Änderungen konnten nicht angewendet werden",
"Manage tags" : "Schlagworte verwalten",
"Applying tags changes…" : "Schlagwort-Änderungen werden angewendet …",
"Applying tags changes…" : "Schlagwort-Änderungen werden angewendet…",
"Search or create tag" : "Schlagwort suchen oder erstellen",
"Search tag" : "Schlagworte suchen",
"Change tag color" : "Schlagwortfarbe ändern",
@@ -108,8 +108,8 @@
"Failed to load tags for file" : "Schlagworte für Datei konnten nicht geladen werden",
"Failed to set tag for file" : "Schlagwort für Datei konnte nicht gesetzt werden",
"Failed to delete tag for file" : "Schlagwort für Datei konnte nicht gelöscht werden",
"Collaborative tags …" : "Kollaborative Schlagworte …",
"Loading …" : "Lade …",
"Loading collaborative tags …" : "Kollaborative Schlagworte laden …"
"Collaborative tags …" : "Kollaborative Schlagworte …",
"Loading …" : "Lade …",
"Loading collaborative tags …" : "Kollaborative Schlagworte laden …"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}

Some files were not shown because too many files have changed in this diff Show More