From f4e2b9a2f4d22b84761964acadf81f26b5380ed4 Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Sat, 17 Oct 2026 01:54:28 +0200 Subject: [PATCH 01/11] fix(l10n): owncloud.online statt alter Produktnamen in Kern- und App-Texten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In core/l10n und apps/*/l10n standen in 68 Texten noch Schreibvarianten des alten Produktnamens – Tippfehler, Großschreibung, Hashtag, kyrillische Umschrift. Alle nennen jetzt owncloud.online, jeweils gleich in .json und .js (90 Dateien, Gegenprobe: .json und .js je Sprache wieder deckungsgleich). Kern (core/l10n): - de, de_AT, de_CH, de_DE: Einzelbenutzermodus „ownClound-Instanz“ → „owncloud.online-Instanz“ - eu: Teilen-Hinweis „ownClud-en“ → „owncloud.online-en“ - mk: Reverse-Proxy-Hinweis „OwnCloud“ → „owncloud.online“ - sl: Hinweis ohne Internetverbindung „OwnCloud“ → „owncloud.online“ - sr: Weiterleitung nach dem Update und Einzelbenutzermodus „оунКлауд“ → „owncloud.online“ - sv: Reverse-Proxy-Hinweis dreimal „Owncloud“ → „owncloud.online“ federatedfilesharing: - es, ja („OwnCloud“), sv („Owncloud“): „Share with me via …“ → „owncloud.online“ - fr, oc: „Share with me through my Federated Cloud ID“ mit und ohne „, see %s“: „#owncloud“ → „#owncloud.online“; sv dasselbe mit „, see %s“ federation: - es „Trusted Servers“ und eu Beschreibung: „onwCloud“ → „owncloud.online“ - es_AR: „No server found“, Beschreibung (zweimal), „Trusted Servers“, „+ Add server“, „Server“: „OwnCloud“ → „owncloud.online“ - es_MX Beschreibung, pt_BR „Trusted Servers“, fr „Server“: „OwnCloud“ → „owncloud.online“ - fr, oc „+ Add server“: „Owncloud“ → „owncloud.online“ - ru, ru_RU „Server added to the list of trusted servers“: „ownClowd“ → „owncloud.online“ - sr, sr@latin: „Server added …“, „No server found“, Beschreibung (zweimal), „Trusted Servers“, „+ Add server“, „Server“: „оунКлауд“ → „owncloud.online“ files_external: - es, ru, ru_RU, ug „Couldn't get the information from the remote server“: „OwnCloud“ → „owncloud.online“ - sr „SMB / CIFS using OC login“: „оунКлауд“ → „owncloud.online“ files_sharing: - de, de_CH, de_DE „No compatible server installation found at {remote}“: „OwnCloud-Installation (7 oder höher)“ → „owncloud.online-Installation“ - pt_PT „instalação OwnCloud (7 ou superior)“ → „instalação owncloud.online“, sv „OwnCloud Installation (7 eller senare)“ → „owncloud.online-installation“, sr „оунКлауд инсталације верзије 7 или више“ → „owncloud.online инсталације“, uk „OwnCloud (7 або вище)“ → „owncloud.online“. Die Versionsangabe gehört zum alten Produkt und steht nicht im Quelltext, sie entfällt. - „Invalid server url“: de, de_CH „OwnCloud-URL“ → „owncloud.online-URL“, de_DE „OwnCloud-Adresse“ → „owncloud.online-Adresse“, es_AR „OwnCloud“, nn_NO „ownClud“, sr „оунКлауд“ → „owncloud.online“ updatenotification: - da, uk Hinweis zum Update-Kanal: „OwnCloud“ → „owncloud.online“ - es_AR „owncloud.online core“: „núcleo wnCloud“ → „núcleo de owncloud.online“ - pt_BR, pt_PT Enterprise-Hinweis: „uma edição OwnCloud Enterprise“ → „uma edição Enterprise“ – der Quelltext nennt dort kein Produkt - ug Enterprise-Hinweis: „ئۆزىڭىزنىڭ Cloud“ (wörtlich „Ihr eigenes Cloud“) → „ئۆزىڭىزنىڭ“, zweimal Unverändert bleiben Beispielpfade wie username@example.com/owncloud (auch gl „…/ouwncloud“), Platzhalter und technische Bezeichner. Test: ProductNameTranslationTest liest alle .json und .js unter core/l10n und apps/*/l10n und sucht die Schreibvarianten (nicht nach „/“, nicht vor „.online“). Vorher 136 Fundstellen (68 Texte in je zwei Dateien), danach keine. --- apps/federatedfilesharing/l10n/es.js | 2 +- apps/federatedfilesharing/l10n/es.json | 2 +- apps/federatedfilesharing/l10n/fr.js | 4 +- apps/federatedfilesharing/l10n/fr.json | 4 +- apps/federatedfilesharing/l10n/ja.js | 2 +- apps/federatedfilesharing/l10n/ja.json | 2 +- apps/federatedfilesharing/l10n/oc.js | 4 +- apps/federatedfilesharing/l10n/oc.json | 4 +- apps/federatedfilesharing/l10n/sv.js | 4 +- apps/federatedfilesharing/l10n/sv.json | 4 +- apps/federation/l10n/es.js | 2 +- apps/federation/l10n/es.json | 2 +- apps/federation/l10n/es_AR.js | 10 +- apps/federation/l10n/es_AR.json | 10 +- apps/federation/l10n/es_MX.js | 2 +- apps/federation/l10n/es_MX.json | 2 +- apps/federation/l10n/eu.js | 2 +- apps/federation/l10n/eu.json | 2 +- apps/federation/l10n/fr.js | 4 +- apps/federation/l10n/fr.json | 4 +- apps/federation/l10n/oc.js | 2 +- apps/federation/l10n/oc.json | 2 +- apps/federation/l10n/pt_BR.js | 2 +- apps/federation/l10n/pt_BR.json | 2 +- apps/federation/l10n/ru.js | 2 +- apps/federation/l10n/ru.json | 2 +- apps/federation/l10n/ru_RU.js | 2 +- apps/federation/l10n/ru_RU.json | 2 +- apps/federation/l10n/sr.js | 12 +- apps/federation/l10n/sr.json | 12 +- apps/federation/l10n/sr@latin.js | 12 +- apps/federation/l10n/sr@latin.json | 12 +- apps/files_external/l10n/es.js | 2 +- apps/files_external/l10n/es.json | 2 +- apps/files_external/l10n/ru.js | 2 +- apps/files_external/l10n/ru.json | 2 +- apps/files_external/l10n/ru_RU.js | 2 +- apps/files_external/l10n/ru_RU.json | 2 +- apps/files_external/l10n/sr.js | 2 +- apps/files_external/l10n/sr.json | 2 +- apps/files_external/l10n/ug.js | 2 +- apps/files_external/l10n/ug.json | 2 +- apps/files_sharing/l10n/de.js | 4 +- apps/files_sharing/l10n/de.json | 4 +- apps/files_sharing/l10n/de_CH.js | 4 +- apps/files_sharing/l10n/de_CH.json | 4 +- apps/files_sharing/l10n/de_DE.js | 4 +- apps/files_sharing/l10n/de_DE.json | 4 +- apps/files_sharing/l10n/es_AR.js | 2 +- apps/files_sharing/l10n/es_AR.json | 2 +- apps/files_sharing/l10n/nn_NO.js | 2 +- apps/files_sharing/l10n/nn_NO.json | 2 +- apps/files_sharing/l10n/pt_PT.js | 2 +- apps/files_sharing/l10n/pt_PT.json | 2 +- apps/files_sharing/l10n/sr.js | 4 +- apps/files_sharing/l10n/sr.json | 4 +- apps/files_sharing/l10n/sv.js | 2 +- apps/files_sharing/l10n/sv.json | 2 +- apps/files_sharing/l10n/uk.js | 2 +- apps/files_sharing/l10n/uk.json | 2 +- apps/updatenotification/l10n/da.js | 2 +- apps/updatenotification/l10n/da.json | 2 +- apps/updatenotification/l10n/es_AR.js | 2 +- apps/updatenotification/l10n/es_AR.json | 2 +- apps/updatenotification/l10n/pt_BR.js | 2 +- apps/updatenotification/l10n/pt_BR.json | 2 +- apps/updatenotification/l10n/pt_PT.js | 2 +- apps/updatenotification/l10n/pt_PT.json | 2 +- apps/updatenotification/l10n/ug.js | 2 +- apps/updatenotification/l10n/ug.json | 2 +- apps/updatenotification/l10n/uk.js | 2 +- apps/updatenotification/l10n/uk.json | 2 +- .../unreleased/l10n-current-product-name | 8 ++ core/l10n/de.js | 2 +- core/l10n/de.json | 2 +- core/l10n/de_AT.js | 2 +- core/l10n/de_AT.json | 2 +- core/l10n/de_CH.js | 2 +- core/l10n/de_CH.json | 2 +- core/l10n/de_DE.js | 2 +- core/l10n/de_DE.json | 2 +- core/l10n/eu.js | 2 +- core/l10n/eu.json | 2 +- core/l10n/mk.js | 2 +- core/l10n/mk.json | 2 +- core/l10n/sl.js | 2 +- core/l10n/sl.json | 2 +- core/l10n/sr.js | 4 +- core/l10n/sr.json | 4 +- core/l10n/sv.js | 2 +- core/l10n/sv.json | 2 +- tests/lib/L10N/ProductNameTranslationTest.php | 121 ++++++++++++++++++ 92 files changed, 265 insertions(+), 136 deletions(-) create mode 100644 changelog/unreleased/l10n-current-product-name create mode 100644 tests/lib/L10N/ProductNameTranslationTest.php diff --git a/apps/federatedfilesharing/l10n/es.js b/apps/federatedfilesharing/l10n/es.js index 4a975e09b5..b6cf670bb1 100644 --- a/apps/federatedfilesharing/l10n/es.js +++ b/apps/federatedfilesharing/l10n/es.js @@ -29,7 +29,7 @@ OC.L10N.register( "Your Federated Cloud ID:" : "Su ID Nube Federada:", "Share it:" : "Compartir:", "Add to your website" : "Añadir a su sitio web", - "Share with me via owncloud.online" : "Compartirlo conmigo vía OwnCloud", + "Share with me via owncloud.online" : "Compartirlo conmigo vía owncloud.online", "HTML Code:" : "Código HTML:", "Nothing to configure." : "Nada que configurar" }, diff --git a/apps/federatedfilesharing/l10n/es.json b/apps/federatedfilesharing/l10n/es.json index 902c8d82a4..dff8cfc4b1 100644 --- a/apps/federatedfilesharing/l10n/es.json +++ b/apps/federatedfilesharing/l10n/es.json @@ -27,7 +27,7 @@ "Your Federated Cloud ID:" : "Su ID Nube Federada:", "Share it:" : "Compartir:", "Add to your website" : "Añadir a su sitio web", - "Share with me via owncloud.online" : "Compartirlo conmigo vía OwnCloud", + "Share with me via owncloud.online" : "Compartirlo conmigo vía owncloud.online", "HTML Code:" : "Código HTML:", "Nothing to configure." : "Nada que configurar" },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" diff --git a/apps/federatedfilesharing/l10n/fr.js b/apps/federatedfilesharing/l10n/fr.js index 60dd7fab54..4ca698706a 100644 --- a/apps/federatedfilesharing/l10n/fr.js +++ b/apps/federatedfilesharing/l10n/fr.js @@ -17,8 +17,8 @@ OC.L10N.register( "\"%1$s\" invited you to view \"%3$s\"" : "\"%1$s\" vous a invité à voir \"%3$s\"", "Accept" : "Accepter", "Decline" : "Refuser", - "Share with me through my Federated Cloud ID, see %s" : "Partagez avec moi grâce à mon identifiant Federated Cloud #owncloud %s", - "Share with me through my Federated Cloud ID" : "Partagez avec moi grâce à mon identifiant Federated Cloud #owncloud", + "Share with me through my Federated Cloud ID, see %s" : "Partagez avec moi grâce à mon identifiant Federated Cloud #owncloud.online %s", + "Share with me through my Federated Cloud ID" : "Partagez avec moi grâce à mon identifiant Federated Cloud #owncloud.online", "Federated Cloud Sharing" : "Federated Cloud Sharing", "Open documentation" : "Voir la documentation", "Periodically synchronize outdated federated shares for active users" : "Synchroniser périodiquement les partages fédérés obsolètes pour les utilisateurs actifs", diff --git a/apps/federatedfilesharing/l10n/fr.json b/apps/federatedfilesharing/l10n/fr.json index e127da36a6..9db4b50fd3 100644 --- a/apps/federatedfilesharing/l10n/fr.json +++ b/apps/federatedfilesharing/l10n/fr.json @@ -15,8 +15,8 @@ "\"%1$s\" invited you to view \"%3$s\"" : "\"%1$s\" vous a invité à voir \"%3$s\"", "Accept" : "Accepter", "Decline" : "Refuser", - "Share with me through my Federated Cloud ID, see %s" : "Partagez avec moi grâce à mon identifiant Federated Cloud #owncloud %s", - "Share with me through my Federated Cloud ID" : "Partagez avec moi grâce à mon identifiant Federated Cloud #owncloud", + "Share with me through my Federated Cloud ID, see %s" : "Partagez avec moi grâce à mon identifiant Federated Cloud #owncloud.online %s", + "Share with me through my Federated Cloud ID" : "Partagez avec moi grâce à mon identifiant Federated Cloud #owncloud.online", "Federated Cloud Sharing" : "Federated Cloud Sharing", "Open documentation" : "Voir la documentation", "Periodically synchronize outdated federated shares for active users" : "Synchroniser périodiquement les partages fédérés obsolètes pour les utilisateurs actifs", diff --git a/apps/federatedfilesharing/l10n/ja.js b/apps/federatedfilesharing/l10n/ja.js index d41eda3543..ff6226fa9e 100644 --- a/apps/federatedfilesharing/l10n/ja.js +++ b/apps/federatedfilesharing/l10n/ja.js @@ -20,7 +20,7 @@ OC.L10N.register( "Your Federated Cloud ID:" : "あなたのクラウド連携ID:", "Share it:" : "以下で共有:", "Add to your website" : "ウェブサイトに追加", - "Share with me via owncloud.online" : "OwnCloud経由で共有", + "Share with me via owncloud.online" : "owncloud.online経由で共有", "HTML Code:" : "HTMLコード:" }, "nplurals=1; plural=0;"); diff --git a/apps/federatedfilesharing/l10n/ja.json b/apps/federatedfilesharing/l10n/ja.json index 334f8dd190..4c2cd992a8 100644 --- a/apps/federatedfilesharing/l10n/ja.json +++ b/apps/federatedfilesharing/l10n/ja.json @@ -18,7 +18,7 @@ "Your Federated Cloud ID:" : "あなたのクラウド連携ID:", "Share it:" : "以下で共有:", "Add to your website" : "ウェブサイトに追加", - "Share with me via owncloud.online" : "OwnCloud経由で共有", + "Share with me via owncloud.online" : "owncloud.online経由で共有", "HTML Code:" : "HTMLコード:" },"pluralForm" :"nplurals=1; plural=0;" } \ No newline at end of file diff --git a/apps/federatedfilesharing/l10n/oc.js b/apps/federatedfilesharing/l10n/oc.js index 3823804f3d..38154856b5 100644 --- a/apps/federatedfilesharing/l10n/oc.js +++ b/apps/federatedfilesharing/l10n/oc.js @@ -10,8 +10,8 @@ OC.L10N.register( "Sharing %s failed, could not find %s, maybe the server is currently unreachable." : "Lo partiment de %s a fracassat : impossible de trobar %s. Benlèu que lo servidor es momentanèament injonhable.", "Accept" : "Acceptar", "Decline" : "Refusar", - "Share with me through my Federated Cloud ID, see %s" : "Partejatz amb ieu gràcias a mon identificant Federated Cloud #owncloud %s", - "Share with me through my Federated Cloud ID" : "Partejatz amb ieu gràcias a mon identificant Federated Cloud #owncloud", + "Share with me through my Federated Cloud ID, see %s" : "Partejatz amb ieu gràcias a mon identificant Federated Cloud #owncloud.online %s", + "Share with me through my Federated Cloud ID" : "Partejatz amb ieu gràcias a mon identificant Federated Cloud #owncloud.online", "Federated Cloud Sharing" : "Federated Cloud Sharing", "Open documentation" : "Veire la documentacion", "Allow users on this server to send shares to other servers" : "Autorizar los utilizaires d'aqueste servidor a mandar de partiments cap a d'autres servidors", diff --git a/apps/federatedfilesharing/l10n/oc.json b/apps/federatedfilesharing/l10n/oc.json index 58877e809a..06b80aef38 100644 --- a/apps/federatedfilesharing/l10n/oc.json +++ b/apps/federatedfilesharing/l10n/oc.json @@ -8,8 +8,8 @@ "Sharing %s failed, could not find %s, maybe the server is currently unreachable." : "Lo partiment de %s a fracassat : impossible de trobar %s. Benlèu que lo servidor es momentanèament injonhable.", "Accept" : "Acceptar", "Decline" : "Refusar", - "Share with me through my Federated Cloud ID, see %s" : "Partejatz amb ieu gràcias a mon identificant Federated Cloud #owncloud %s", - "Share with me through my Federated Cloud ID" : "Partejatz amb ieu gràcias a mon identificant Federated Cloud #owncloud", + "Share with me through my Federated Cloud ID, see %s" : "Partejatz amb ieu gràcias a mon identificant Federated Cloud #owncloud.online %s", + "Share with me through my Federated Cloud ID" : "Partejatz amb ieu gràcias a mon identificant Federated Cloud #owncloud.online", "Federated Cloud Sharing" : "Federated Cloud Sharing", "Open documentation" : "Veire la documentacion", "Allow users on this server to send shares to other servers" : "Autorizar los utilizaires d'aqueste servidor a mandar de partiments cap a d'autres servidors", diff --git a/apps/federatedfilesharing/l10n/sv.js b/apps/federatedfilesharing/l10n/sv.js index 6967062089..b1e087e743 100644 --- a/apps/federatedfilesharing/l10n/sv.js +++ b/apps/federatedfilesharing/l10n/sv.js @@ -16,7 +16,7 @@ OC.L10N.register( "\"%1$s\" invited you to view \"%3$s\"" : "\"%1$s\" bjöd in dig att visa \"%3$s\"", "Accept" : "Acceptera", "Decline" : "Neka", - "Share with me through my Federated Cloud ID, see %s" : "Dela med mig genom min #owncloud Federated Cloud ID, se %s", + "Share with me through my Federated Cloud ID, see %s" : "Dela med mig genom min #owncloud.online Federated Cloud ID, se %s", "Share with me through my Federated Cloud ID" : "Dela med mig genom min #owncloud.online Federated Cloud ID", "Federated Cloud Sharing" : "Federate Cloud delning", "Open documentation" : "Öppna dokumentation", @@ -26,7 +26,7 @@ OC.L10N.register( "Your Federated Cloud ID:" : "Ditt Federated Cloud ID", "Share it:" : "Dela detta:", "Add to your website" : "Lägg till på din hemsida", - "Share with me via owncloud.online" : "Dela med mig via Owncloud", + "Share with me via owncloud.online" : "Dela med mig via owncloud.online", "HTML Code:" : "HTML Kod:", "Nothing to configure." : "Inget att konfigurera." }, diff --git a/apps/federatedfilesharing/l10n/sv.json b/apps/federatedfilesharing/l10n/sv.json index 01c2ac9701..26e921ee9c 100644 --- a/apps/federatedfilesharing/l10n/sv.json +++ b/apps/federatedfilesharing/l10n/sv.json @@ -14,7 +14,7 @@ "\"%1$s\" invited you to view \"%3$s\"" : "\"%1$s\" bjöd in dig att visa \"%3$s\"", "Accept" : "Acceptera", "Decline" : "Neka", - "Share with me through my Federated Cloud ID, see %s" : "Dela med mig genom min #owncloud Federated Cloud ID, se %s", + "Share with me through my Federated Cloud ID, see %s" : "Dela med mig genom min #owncloud.online Federated Cloud ID, se %s", "Share with me through my Federated Cloud ID" : "Dela med mig genom min #owncloud.online Federated Cloud ID", "Federated Cloud Sharing" : "Federate Cloud delning", "Open documentation" : "Öppna dokumentation", @@ -24,7 +24,7 @@ "Your Federated Cloud ID:" : "Ditt Federated Cloud ID", "Share it:" : "Dela detta:", "Add to your website" : "Lägg till på din hemsida", - "Share with me via owncloud.online" : "Dela med mig via Owncloud", + "Share with me via owncloud.online" : "Dela med mig via owncloud.online", "HTML Code:" : "HTML Kod:", "Nothing to configure." : "Inget att konfigurera." },"pluralForm" :"nplurals=2; plural=(n != 1);" diff --git a/apps/federation/l10n/es.js b/apps/federation/l10n/es.js index 12d4897b36..616afc8c91 100644 --- a/apps/federation/l10n/es.js +++ b/apps/federation/l10n/es.js @@ -8,7 +8,7 @@ OC.L10N.register( "Federation" : "Federación", "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "La Federación de owncloud.online permite conectar con otros owncloud.onlines en los que se confíe para intercambiar el directorio de usuario. Por ejemplo esto se puede usar para autocompletar usuarios externos en caso de compartición federada.", "Add server automatically once a federated share was created successfully" : "Añadir el servidor automáticamente una vez que un compartido federado se haya creado exitosamente", - "Trusted Servers" : "Servidores onwCloud en los que confío", + "Trusted Servers" : "Servidores owncloud.online en los que confío", "+ Add server" : "+ Añadir servidor owncloud.online", "Server" : "Servidor owncloud.online", "Add" : "Añadir" diff --git a/apps/federation/l10n/es.json b/apps/federation/l10n/es.json index eb1543fcad..9139ee9295 100644 --- a/apps/federation/l10n/es.json +++ b/apps/federation/l10n/es.json @@ -6,7 +6,7 @@ "Federation" : "Federación", "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "La Federación de owncloud.online permite conectar con otros owncloud.onlines en los que se confíe para intercambiar el directorio de usuario. Por ejemplo esto se puede usar para autocompletar usuarios externos en caso de compartición federada.", "Add server automatically once a federated share was created successfully" : "Añadir el servidor automáticamente una vez que un compartido federado se haya creado exitosamente", - "Trusted Servers" : "Servidores onwCloud en los que confío", + "Trusted Servers" : "Servidores owncloud.online en los que confío", "+ Add server" : "+ Añadir servidor owncloud.online", "Server" : "Servidor owncloud.online", "Add" : "Añadir" diff --git a/apps/federation/l10n/es_AR.js b/apps/federation/l10n/es_AR.js index f5be0ec994..5c6cb8b740 100644 --- a/apps/federation/l10n/es_AR.js +++ b/apps/federation/l10n/es_AR.js @@ -3,14 +3,14 @@ OC.L10N.register( { "Server added to the list of trusted servers" : "Servidor añadido a la lista de confianza de owncloud.online", "Server is already in the list of trusted servers." : "El Servidor ya está en la lista de servidores de confianza.", - "No server found" : "No se ha encontrado un servidor de OwnCloud", + "No server found" : "No se ha encontrado un servidor de owncloud.online", "Could not add server" : "No se pudo agregar el servidor", "Federation" : "Federación", - "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "La Federación OwnCloud, te permite a tí conectarte con otros servidores OwnCloud confiables para intercambiar carpetas de usuario. Esto puede ser utilizado, por ejemplo para auto-completar usuarios externos al compartir dentro de la Federación.", + "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "La Federación owncloud.online, te permite a tí conectarte con otros servidores owncloud.online confiables para intercambiar carpetas de usuario. Esto puede ser utilizado, por ejemplo para auto-completar usuarios externos al compartir dentro de la Federación.", "Add server automatically once a federated share was created successfully" : "Agregar automáticamente el servidor una vez que el compartir federado fue creado satisfactoriamente", - "Trusted Servers" : "Servidores OwnCloud de confianza", - "+ Add server" : "+ Agregar servidor OwnCloud", - "Server" : "Servidor OwnCloud", + "Trusted Servers" : "Servidores owncloud.online de confianza", + "+ Add server" : "+ Agregar servidor owncloud.online", + "Server" : "Servidor owncloud.online", "Add" : "Agregar" }, "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/federation/l10n/es_AR.json b/apps/federation/l10n/es_AR.json index a370ba92b9..81af0b0bef 100644 --- a/apps/federation/l10n/es_AR.json +++ b/apps/federation/l10n/es_AR.json @@ -1,14 +1,14 @@ { "translations": { "Server added to the list of trusted servers" : "Servidor añadido a la lista de confianza de owncloud.online", "Server is already in the list of trusted servers." : "El Servidor ya está en la lista de servidores de confianza.", - "No server found" : "No se ha encontrado un servidor de OwnCloud", + "No server found" : "No se ha encontrado un servidor de owncloud.online", "Could not add server" : "No se pudo agregar el servidor", "Federation" : "Federación", - "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "La Federación OwnCloud, te permite a tí conectarte con otros servidores OwnCloud confiables para intercambiar carpetas de usuario. Esto puede ser utilizado, por ejemplo para auto-completar usuarios externos al compartir dentro de la Federación.", + "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "La Federación owncloud.online, te permite a tí conectarte con otros servidores owncloud.online confiables para intercambiar carpetas de usuario. Esto puede ser utilizado, por ejemplo para auto-completar usuarios externos al compartir dentro de la Federación.", "Add server automatically once a federated share was created successfully" : "Agregar automáticamente el servidor una vez que el compartir federado fue creado satisfactoriamente", - "Trusted Servers" : "Servidores OwnCloud de confianza", - "+ Add server" : "+ Agregar servidor OwnCloud", - "Server" : "Servidor OwnCloud", + "Trusted Servers" : "Servidores owncloud.online de confianza", + "+ Add server" : "+ Agregar servidor owncloud.online", + "Server" : "Servidor owncloud.online", "Add" : "Agregar" },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/apps/federation/l10n/es_MX.js b/apps/federation/l10n/es_MX.js index f77cfcdf2f..111be30848 100644 --- a/apps/federation/l10n/es_MX.js +++ b/apps/federation/l10n/es_MX.js @@ -6,7 +6,7 @@ OC.L10N.register( "No server found" : "No se encontró ningún servidor owncloud.online", "Could not add server" : "No se puedo añadir servidor", "Federation" : "Federación", - "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "OwnCloud Federation le permite conectarse con otros owncloud.onlines de confianza para intercambiar el directorio de usuarios. Por ejemplo, esto se utilizará para autocompletar usuarios externos para el uso compartido federado.", + "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "owncloud.online Federation le permite conectarse con otros owncloud.onlines de confianza para intercambiar el directorio de usuarios. Por ejemplo, esto se utilizará para autocompletar usuarios externos para el uso compartido federado.", "Add server automatically once a federated share was created successfully" : "Agregar servidor automáticamente una vez que se creó correctamente un recurso compartido federado", "Trusted Servers" : "Servidores owncloud.online de confianza", "+ Add server" : "+ Agregar servidor owncloud.online", diff --git a/apps/federation/l10n/es_MX.json b/apps/federation/l10n/es_MX.json index 6d924134d1..fa5dee76cd 100644 --- a/apps/federation/l10n/es_MX.json +++ b/apps/federation/l10n/es_MX.json @@ -4,7 +4,7 @@ "No server found" : "No se encontró ningún servidor owncloud.online", "Could not add server" : "No se puedo añadir servidor", "Federation" : "Federación", - "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "OwnCloud Federation le permite conectarse con otros owncloud.onlines de confianza para intercambiar el directorio de usuarios. Por ejemplo, esto se utilizará para autocompletar usuarios externos para el uso compartido federado.", + "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "owncloud.online Federation le permite conectarse con otros owncloud.onlines de confianza para intercambiar el directorio de usuarios. Por ejemplo, esto se utilizará para autocompletar usuarios externos para el uso compartido federado.", "Add server automatically once a federated share was created successfully" : "Agregar servidor automáticamente una vez que se creó correctamente un recurso compartido federado", "Trusted Servers" : "Servidores owncloud.online de confianza", "+ Add server" : "+ Agregar servidor owncloud.online", diff --git a/apps/federation/l10n/eu.js b/apps/federation/l10n/eu.js index 5d66862a52..12c6ffc7b2 100644 --- a/apps/federation/l10n/eu.js +++ b/apps/federation/l10n/eu.js @@ -6,7 +6,7 @@ OC.L10N.register( "No server found" : "Ez da owncloud.online zerbitzaririk aurkitu", "Could not add server" : "Ezin izan da zerbitzaria sartu", "Federation" : "Federazioa", - "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "owncloud.online Federazioak beste onwCloud fidagarri batzuekin konektatzea posible egiten du erabiltzailearen karpeta trukatzeko. Hau kanpo erabiltzaileen partekatze federaturako auto-osatzeko gisa erabili daiteke.", + "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "owncloud.online Federazioak beste owncloud.online fidagarri batzuekin konektatzea posible egiten du erabiltzailearen karpeta trukatzeko. Hau kanpo erabiltzaileen partekatze federaturako auto-osatzeko gisa erabili daiteke.", "Add server automatically once a federated share was created successfully" : "Gehitu zerbitzaria automatikoki federatutako partekatzea ondo sortzen denean.", "Trusted Servers" : "owncloud.online zerbitzari fidagarriak", "+ Add server" : "+ Gehitu owncloud.online zerbitzaria", diff --git a/apps/federation/l10n/eu.json b/apps/federation/l10n/eu.json index bde4071687..d66841481c 100644 --- a/apps/federation/l10n/eu.json +++ b/apps/federation/l10n/eu.json @@ -4,7 +4,7 @@ "No server found" : "Ez da owncloud.online zerbitzaririk aurkitu", "Could not add server" : "Ezin izan da zerbitzaria sartu", "Federation" : "Federazioa", - "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "owncloud.online Federazioak beste onwCloud fidagarri batzuekin konektatzea posible egiten du erabiltzailearen karpeta trukatzeko. Hau kanpo erabiltzaileen partekatze federaturako auto-osatzeko gisa erabili daiteke.", + "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "owncloud.online Federazioak beste owncloud.online fidagarri batzuekin konektatzea posible egiten du erabiltzailearen karpeta trukatzeko. Hau kanpo erabiltzaileen partekatze federaturako auto-osatzeko gisa erabili daiteke.", "Add server automatically once a federated share was created successfully" : "Gehitu zerbitzaria automatikoki federatutako partekatzea ondo sortzen denean.", "Trusted Servers" : "owncloud.online zerbitzari fidagarriak", "+ Add server" : "+ Gehitu owncloud.online zerbitzaria", diff --git a/apps/federation/l10n/fr.js b/apps/federation/l10n/fr.js index f8da374dde..fa4682746c 100644 --- a/apps/federation/l10n/fr.js +++ b/apps/federation/l10n/fr.js @@ -9,8 +9,8 @@ OC.L10N.register( "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "La Fédération owncloud.online permet la connexion à d'autres instances owncloud.online de confiance pour partager les annuaires d'utilisateurs. Cela peut par exemple être utilisé pour compléter automatiquement les noms des utilisateurs externes lors d'un partage vers une autre instance.", "Add server automatically once a federated share was created successfully" : "Ajouter un serveur automatiquement une fois que le partage a été créé avec succès", "Trusted Servers" : "Serveur owncloud.online de confiance ", - "+ Add server" : "Ajouter un serveur Owncloud", - "Server" : "Serveur OwnCloud", + "+ Add server" : "Ajouter un serveur owncloud.online", + "Server" : "Serveur owncloud.online", "Add" : "Ajouter" }, "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/federation/l10n/fr.json b/apps/federation/l10n/fr.json index 40dba3833e..349924f74a 100644 --- a/apps/federation/l10n/fr.json +++ b/apps/federation/l10n/fr.json @@ -7,8 +7,8 @@ "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "La Fédération owncloud.online permet la connexion à d'autres instances owncloud.online de confiance pour partager les annuaires d'utilisateurs. Cela peut par exemple être utilisé pour compléter automatiquement les noms des utilisateurs externes lors d'un partage vers une autre instance.", "Add server automatically once a federated share was created successfully" : "Ajouter un serveur automatiquement une fois que le partage a été créé avec succès", "Trusted Servers" : "Serveur owncloud.online de confiance ", - "+ Add server" : "Ajouter un serveur Owncloud", - "Server" : "Serveur OwnCloud", + "+ Add server" : "Ajouter un serveur owncloud.online", + "Server" : "Serveur owncloud.online", "Add" : "Ajouter" },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/apps/federation/l10n/oc.js b/apps/federation/l10n/oc.js index 800a1be77f..92754a2098 100644 --- a/apps/federation/l10n/oc.js +++ b/apps/federation/l10n/oc.js @@ -9,7 +9,7 @@ OC.L10N.register( "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "La Federacion owncloud.online permet la connexion a d'autras instàncias owncloud.online fisablas per partejar los annuaris d'utilizaires. Aquò pòt, per exemple, èsser utilizat per completar automaticament los noms dels utilizaires extèrnes al moment d'un partiment cap a una autra instància.", "Add server automatically once a federated share was created successfully" : "Apondre un servidor automaticament un còp que lo partiment es estat creat amb succès", "Trusted Servers" : "Servidor owncloud.online fisable ", - "+ Add server" : "+ Apondre un servidor Owncloud", + "+ Add server" : "+ Apondre un servidor owncloud.online", "Server" : "Servidor owncloud.online", "Add" : "Apondre" }, diff --git a/apps/federation/l10n/oc.json b/apps/federation/l10n/oc.json index b74ec99a77..b731c02d36 100644 --- a/apps/federation/l10n/oc.json +++ b/apps/federation/l10n/oc.json @@ -7,7 +7,7 @@ "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "La Federacion owncloud.online permet la connexion a d'autras instàncias owncloud.online fisablas per partejar los annuaris d'utilizaires. Aquò pòt, per exemple, èsser utilizat per completar automaticament los noms dels utilizaires extèrnes al moment d'un partiment cap a una autra instància.", "Add server automatically once a federated share was created successfully" : "Apondre un servidor automaticament un còp que lo partiment es estat creat amb succès", "Trusted Servers" : "Servidor owncloud.online fisable ", - "+ Add server" : "+ Apondre un servidor Owncloud", + "+ Add server" : "+ Apondre un servidor owncloud.online", "Server" : "Servidor owncloud.online", "Add" : "Apondre" },"pluralForm" :"nplurals=2; plural=(n > 1);" diff --git a/apps/federation/l10n/pt_BR.js b/apps/federation/l10n/pt_BR.js index f0f1ba9916..91700111f0 100644 --- a/apps/federation/l10n/pt_BR.js +++ b/apps/federation/l10n/pt_BR.js @@ -8,7 +8,7 @@ OC.L10N.register( "Federation" : "Associação", "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "A associação owncloud.online permite que você conecte com outros owncloud.online confiáveis para que haja troca de diretórios do usuário. Por exemplo, este será utilizado para usuários externos de complementação para compartilhamento associado.", "Add server automatically once a federated share was created successfully" : "Adicionar servidor automaticamente uma vez que um compartilhamento associado foi criado com êxito", - "Trusted Servers" : "Servidores OwnCloud Confiáveis", + "Trusted Servers" : "Servidores owncloud.online Confiáveis", "+ Add server" : "+ Adicionar servidor owncloud.online", "Server" : "Servidor owncloud.online", "Add" : "Adicionar" diff --git a/apps/federation/l10n/pt_BR.json b/apps/federation/l10n/pt_BR.json index 5538eaca05..8df7bdc1c5 100644 --- a/apps/federation/l10n/pt_BR.json +++ b/apps/federation/l10n/pt_BR.json @@ -6,7 +6,7 @@ "Federation" : "Associação", "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "A associação owncloud.online permite que você conecte com outros owncloud.online confiáveis para que haja troca de diretórios do usuário. Por exemplo, este será utilizado para usuários externos de complementação para compartilhamento associado.", "Add server automatically once a federated share was created successfully" : "Adicionar servidor automaticamente uma vez que um compartilhamento associado foi criado com êxito", - "Trusted Servers" : "Servidores OwnCloud Confiáveis", + "Trusted Servers" : "Servidores owncloud.online Confiáveis", "+ Add server" : "+ Adicionar servidor owncloud.online", "Server" : "Servidor owncloud.online", "Add" : "Adicionar" diff --git a/apps/federation/l10n/ru.js b/apps/federation/l10n/ru.js index 002a70752f..1a919cd800 100644 --- a/apps/federation/l10n/ru.js +++ b/apps/federation/l10n/ru.js @@ -1,7 +1,7 @@ OC.L10N.register( "federation", { - "Server added to the list of trusted servers" : "Сервер добавлен в список доверенных серверов ownClowd ", + "Server added to the list of trusted servers" : "Сервер добавлен в список доверенных серверов owncloud.online ", "Server is already in the list of trusted servers." : "Сервер уже в списке доверенных серверов.", "No server found" : "Сервер owncloud.online не найден", "Could not add server" : "Не удалось добавить сервер", diff --git a/apps/federation/l10n/ru.json b/apps/federation/l10n/ru.json index 48ec3d9a71..7eb7a7e38d 100644 --- a/apps/federation/l10n/ru.json +++ b/apps/federation/l10n/ru.json @@ -1,5 +1,5 @@ { "translations": { - "Server added to the list of trusted servers" : "Сервер добавлен в список доверенных серверов ownClowd ", + "Server added to the list of trusted servers" : "Сервер добавлен в список доверенных серверов owncloud.online ", "Server is already in the list of trusted servers." : "Сервер уже в списке доверенных серверов.", "No server found" : "Сервер owncloud.online не найден", "Could not add server" : "Не удалось добавить сервер", diff --git a/apps/federation/l10n/ru_RU.js b/apps/federation/l10n/ru_RU.js index 002a70752f..1a919cd800 100644 --- a/apps/federation/l10n/ru_RU.js +++ b/apps/federation/l10n/ru_RU.js @@ -1,7 +1,7 @@ OC.L10N.register( "federation", { - "Server added to the list of trusted servers" : "Сервер добавлен в список доверенных серверов ownClowd ", + "Server added to the list of trusted servers" : "Сервер добавлен в список доверенных серверов owncloud.online ", "Server is already in the list of trusted servers." : "Сервер уже в списке доверенных серверов.", "No server found" : "Сервер owncloud.online не найден", "Could not add server" : "Не удалось добавить сервер", diff --git a/apps/federation/l10n/ru_RU.json b/apps/federation/l10n/ru_RU.json index 48ec3d9a71..7eb7a7e38d 100644 --- a/apps/federation/l10n/ru_RU.json +++ b/apps/federation/l10n/ru_RU.json @@ -1,5 +1,5 @@ { "translations": { - "Server added to the list of trusted servers" : "Сервер добавлен в список доверенных серверов ownClowd ", + "Server added to the list of trusted servers" : "Сервер добавлен в список доверенных серверов owncloud.online ", "Server is already in the list of trusted servers." : "Сервер уже в списке доверенных серверов.", "No server found" : "Сервер owncloud.online не найден", "Could not add server" : "Не удалось добавить сервер", diff --git a/apps/federation/l10n/sr.js b/apps/federation/l10n/sr.js index 619e0c6ac4..580b293013 100644 --- a/apps/federation/l10n/sr.js +++ b/apps/federation/l10n/sr.js @@ -1,16 +1,16 @@ OC.L10N.register( "federation", { - "Server added to the list of trusted servers" : "Сервер додат на списак поверљивих оунКлауд сервера", + "Server added to the list of trusted servers" : "Сервер додат на списак поверљивих owncloud.online сервера", "Server is already in the list of trusted servers." : "Сервер је већ на списку поверљивих сервера.", - "No server found" : "Ниједан оунКлауд сервер није нађен", + "No server found" : "Ниједан owncloud.online сервер није нађен", "Could not add server" : "Не могох да додам сервер", "Federation" : "Здруживање", - "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "оунКлауд здруживање вам омогућује да се повежете са другим поверљивим оунКлауд серверима да бисте разменили кориснички директоријум. На пример, ово ће да се користи за аутоматско довршавање спољашњих корисника за здружено дељење.", + "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "owncloud.online здруживање вам омогућује да се повежете са другим поверљивим owncloud.online серверима да бисте разменили кориснички директоријум. На пример, ово ће да се користи за аутоматско довршавање спољашњих корисника за здружено дељење.", "Add server automatically once a federated share was created successfully" : "Додај сервер аутоматски по успешном прављењу здруженог дељења", - "Trusted Servers" : "Поверљиви оунКлауд сервери", - "+ Add server" : "+ Додај оунКлауд сервер", - "Server" : "оунКлауд сервер", + "Trusted Servers" : "Поверљиви owncloud.online сервери", + "+ Add server" : "+ Додај owncloud.online сервер", + "Server" : "owncloud.online сервер", "Add" : "Додај" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/apps/federation/l10n/sr.json b/apps/federation/l10n/sr.json index 96d9e78865..4f7318781c 100644 --- a/apps/federation/l10n/sr.json +++ b/apps/federation/l10n/sr.json @@ -1,14 +1,14 @@ { "translations": { - "Server added to the list of trusted servers" : "Сервер додат на списак поверљивих оунКлауд сервера", + "Server added to the list of trusted servers" : "Сервер додат на списак поверљивих owncloud.online сервера", "Server is already in the list of trusted servers." : "Сервер је већ на списку поверљивих сервера.", - "No server found" : "Ниједан оунКлауд сервер није нађен", + "No server found" : "Ниједан owncloud.online сервер није нађен", "Could not add server" : "Не могох да додам сервер", "Federation" : "Здруживање", - "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "оунКлауд здруживање вам омогућује да се повежете са другим поверљивим оунКлауд серверима да бисте разменили кориснички директоријум. На пример, ово ће да се користи за аутоматско довршавање спољашњих корисника за здружено дељење.", + "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "owncloud.online здруживање вам омогућује да се повежете са другим поверљивим owncloud.online серверима да бисте разменили кориснички директоријум. На пример, ово ће да се користи за аутоматско довршавање спољашњих корисника за здружено дељење.", "Add server automatically once a federated share was created successfully" : "Додај сервер аутоматски по успешном прављењу здруженог дељења", - "Trusted Servers" : "Поверљиви оунКлауд сервери", - "+ Add server" : "+ Додај оунКлауд сервер", - "Server" : "оунКлауд сервер", + "Trusted Servers" : "Поверљиви owncloud.online сервери", + "+ Add server" : "+ Додај owncloud.online сервер", + "Server" : "owncloud.online сервер", "Add" : "Додај" },"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" } \ No newline at end of file diff --git a/apps/federation/l10n/sr@latin.js b/apps/federation/l10n/sr@latin.js index 7c582d3d62..3675525174 100644 --- a/apps/federation/l10n/sr@latin.js +++ b/apps/federation/l10n/sr@latin.js @@ -1,16 +1,16 @@ OC.L10N.register( "federation", { - "Server added to the list of trusted servers" : "Сервер додат на списак поверљивих оунКлауд сервера", + "Server added to the list of trusted servers" : "Сервер додат на списак поверљивих owncloud.online сервера", "Server is already in the list of trusted servers." : "Сервер је већ на списку поверљивих сервера.", - "No server found" : "Ниједан оунКлауд сервер није нађен", + "No server found" : "Ниједан owncloud.online сервер није нађен", "Could not add server" : "Не могох да додам сервер", "Federation" : "Здруживање", - "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "оунКлауд здруживање вам омогућује да се повежете са другим поверљивим оунКлауд серверима да бисте разменили кориснички директоријум. На пример, ово ће да се користи за аутоматско довршавање спољашњих корисника за здружено дељење.", + "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "owncloud.online здруживање вам омогућује да се повежете са другим поверљивим owncloud.online серверима да бисте разменили кориснички директоријум. На пример, ово ће да се користи за аутоматско довршавање спољашњих корисника за здружено дељење.", "Add server automatically once a federated share was created successfully" : "Додај сервер аутоматски по успешном прављењу здруженог дељења", - "Trusted Servers" : "Поверљиви оунКлауд сервери", - "+ Add server" : "+ Додај оунКлауд сервер", - "Server" : "оунКлауд сервер", + "Trusted Servers" : "Поверљиви owncloud.online сервери", + "+ Add server" : "+ Додај owncloud.online сервер", + "Server" : "owncloud.online сервер", "Add" : "Dodaj" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/apps/federation/l10n/sr@latin.json b/apps/federation/l10n/sr@latin.json index c5dc409bd9..0dfefbe70c 100644 --- a/apps/federation/l10n/sr@latin.json +++ b/apps/federation/l10n/sr@latin.json @@ -1,14 +1,14 @@ { "translations": { - "Server added to the list of trusted servers" : "Сервер додат на списак поверљивих оунКлауд сервера", + "Server added to the list of trusted servers" : "Сервер додат на списак поверљивих owncloud.online сервера", "Server is already in the list of trusted servers." : "Сервер је већ на списку поверљивих сервера.", - "No server found" : "Ниједан оунКлауд сервер није нађен", + "No server found" : "Ниједан owncloud.online сервер није нађен", "Could not add server" : "Не могох да додам сервер", "Federation" : "Здруживање", - "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "оунКлауд здруживање вам омогућује да се повежете са другим поверљивим оунКлауд серверима да бисте разменили кориснички директоријум. На пример, ово ће да се користи за аутоматско довршавање спољашњих корисника за здружено дељење.", + "Federation allows you to connect with other trusted servers to exchange the user directory. For example this will be used to auto-complete external users for federated sharing." : "owncloud.online здруживање вам омогућује да се повежете са другим поверљивим owncloud.online серверима да бисте разменили кориснички директоријум. На пример, ово ће да се користи за аутоматско довршавање спољашњих корисника за здружено дељење.", "Add server automatically once a federated share was created successfully" : "Додај сервер аутоматски по успешном прављењу здруженог дељења", - "Trusted Servers" : "Поверљиви оунКлауд сервери", - "+ Add server" : "+ Додај оунКлауд сервер", - "Server" : "оунКлауд сервер", + "Trusted Servers" : "Поверљиви owncloud.online сервери", + "+ Add server" : "+ Додај owncloud.online сервер", + "Server" : "owncloud.online сервер", "Add" : "Dodaj" },"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" } \ No newline at end of file diff --git a/apps/files_external/l10n/es.js b/apps/files_external/l10n/es.js index 75b14f421a..bb73a4d604 100644 --- a/apps/files_external/l10n/es.js +++ b/apps/files_external/l10n/es.js @@ -32,7 +32,7 @@ OC.L10N.register( "Error getting OAuth2 URL for " : "Error obteniendo la URL OAuth2 para", "Empty response from the server" : "Respuesta vacía desde el servidor", "Couldn't access. Please logout and login to activate this mount point" : "No se puede acceder. Por favor cierra sesión e iníciala de nuevo para activar este punto de montaje", - "Couldn't get the information from the remote server: {code} {type}" : "No se puede obtener información acerca del servidor de OwnCloud: {code} {type}", + "Couldn't get the information from the remote server: {code} {type}" : "No se puede obtener información acerca del servidor de owncloud.online: {code} {type}", "Couldn't get the list of external mount points: {type}" : "No se puede obtener la lista de los puntos de montaje externos: {type}", "There was an error with message: " : "Hubo un error con el mensaje:", "External mount error" : "Error de montaje externo", diff --git a/apps/files_external/l10n/es.json b/apps/files_external/l10n/es.json index 8e3399afce..ea27d30eca 100644 --- a/apps/files_external/l10n/es.json +++ b/apps/files_external/l10n/es.json @@ -30,7 +30,7 @@ "Error getting OAuth2 URL for " : "Error obteniendo la URL OAuth2 para", "Empty response from the server" : "Respuesta vacía desde el servidor", "Couldn't access. Please logout and login to activate this mount point" : "No se puede acceder. Por favor cierra sesión e iníciala de nuevo para activar este punto de montaje", - "Couldn't get the information from the remote server: {code} {type}" : "No se puede obtener información acerca del servidor de OwnCloud: {code} {type}", + "Couldn't get the information from the remote server: {code} {type}" : "No se puede obtener información acerca del servidor de owncloud.online: {code} {type}", "Couldn't get the list of external mount points: {type}" : "No se puede obtener la lista de los puntos de montaje externos: {type}", "There was an error with message: " : "Hubo un error con el mensaje:", "External mount error" : "Error de montaje externo", diff --git a/apps/files_external/l10n/ru.js b/apps/files_external/l10n/ru.js index 4e55641150..7346821b8f 100644 --- a/apps/files_external/l10n/ru.js +++ b/apps/files_external/l10n/ru.js @@ -32,7 +32,7 @@ OC.L10N.register( "Error getting OAuth2 URL for " : "Ошибка получения адреса OAuth2 для ", "Empty response from the server" : "Пустой ответ от сервера", "Couldn't access. Please logout and login to activate this mount point" : "Не удалось получить доступ. Пожалуйста, выйти и войдите чтобы активировать эту точку монтирования", - "Couldn't get the information from the remote server: {code} {type}" : "Не удалось получить информацию от сервера OwnCloud: {code} {type}", + "Couldn't get the information from the remote server: {code} {type}" : "Не удалось получить информацию от сервера owncloud.online: {code} {type}", "Couldn't get the list of external mount points: {type}" : "Не удалось получить список внешних точек монтирования: {type}", "There was an error with message: " : "Обнаружена ошибка с сообщением:", "External mount error" : "Ошибка внешнего монтирования", diff --git a/apps/files_external/l10n/ru.json b/apps/files_external/l10n/ru.json index 2801366ae8..ccfb6bc062 100644 --- a/apps/files_external/l10n/ru.json +++ b/apps/files_external/l10n/ru.json @@ -30,7 +30,7 @@ "Error getting OAuth2 URL for " : "Ошибка получения адреса OAuth2 для ", "Empty response from the server" : "Пустой ответ от сервера", "Couldn't access. Please logout and login to activate this mount point" : "Не удалось получить доступ. Пожалуйста, выйти и войдите чтобы активировать эту точку монтирования", - "Couldn't get the information from the remote server: {code} {type}" : "Не удалось получить информацию от сервера OwnCloud: {code} {type}", + "Couldn't get the information from the remote server: {code} {type}" : "Не удалось получить информацию от сервера owncloud.online: {code} {type}", "Couldn't get the list of external mount points: {type}" : "Не удалось получить список внешних точек монтирования: {type}", "There was an error with message: " : "Обнаружена ошибка с сообщением:", "External mount error" : "Ошибка внешнего монтирования", diff --git a/apps/files_external/l10n/ru_RU.js b/apps/files_external/l10n/ru_RU.js index 58165cb3be..0e64709d86 100644 --- a/apps/files_external/l10n/ru_RU.js +++ b/apps/files_external/l10n/ru_RU.js @@ -32,7 +32,7 @@ OC.L10N.register( "Error getting OAuth2 URL for " : "Ошибка получения адреса OAuth2 для ", "Empty response from the server" : "Пустой ответ от сервера", "Couldn't access. Please logout and login to activate this mount point" : "Не удалось получить доступ. Пожалуйста, выйти и войдите чтобы активировать эту точку монтирования", - "Couldn't get the information from the remote server: {code} {type}" : "Не удалось получить информацию от сервера OwnCloud: {code} {type}", + "Couldn't get the information from the remote server: {code} {type}" : "Не удалось получить информацию от сервера owncloud.online: {code} {type}", "Couldn't get the list of external mount points: {type}" : "Не удалось получить список внешних точек монтирования: {type}", "There was an error with message: " : "Обнаружена ошибка с сообщением:", "External mount error" : "Ошибка внешнего монтирования", diff --git a/apps/files_external/l10n/ru_RU.json b/apps/files_external/l10n/ru_RU.json index 21f6ae71e5..280b8e6c8e 100644 --- a/apps/files_external/l10n/ru_RU.json +++ b/apps/files_external/l10n/ru_RU.json @@ -30,7 +30,7 @@ "Error getting OAuth2 URL for " : "Ошибка получения адреса OAuth2 для ", "Empty response from the server" : "Пустой ответ от сервера", "Couldn't access. Please logout and login to activate this mount point" : "Не удалось получить доступ. Пожалуйста, выйти и войдите чтобы активировать эту точку монтирования", - "Couldn't get the information from the remote server: {code} {type}" : "Не удалось получить информацию от сервера OwnCloud: {code} {type}", + "Couldn't get the information from the remote server: {code} {type}" : "Не удалось получить информацию от сервера owncloud.online: {code} {type}", "Couldn't get the list of external mount points: {type}" : "Не удалось получить список внешних точек монтирования: {type}", "There was an error with message: " : "Обнаружена ошибка с сообщением:", "External mount error" : "Ошибка внешнего монтирования", diff --git a/apps/files_external/l10n/sr.js b/apps/files_external/l10n/sr.js index a8e1bb51c6..80fe85eac1 100644 --- a/apps/files_external/l10n/sr.js +++ b/apps/files_external/l10n/sr.js @@ -37,7 +37,7 @@ OC.L10N.register( "SFTP with secret key login" : "СФТП са пријавом помоћу тајног кључа", "Share" : "Дели", "Domain" : "Домен", - "SMB / CIFS using OC login" : "СМБ/ЦИФС користећи оунКлауд пријаву", + "SMB / CIFS using OC login" : "СМБ/ЦИФС користећи owncloud.online пријаву", "Username as share" : "Корисничко име као дељење", "Note: " : "Напомена: ", "Note: The cURL support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it." : "Напомена: cURL подршка за ПХП није омогућена или инсталирана. Монтирање %s није могуће. Затражите од вашег администратора система да је инсталира.", diff --git a/apps/files_external/l10n/sr.json b/apps/files_external/l10n/sr.json index bcd3f0c908..6a9ec8a215 100644 --- a/apps/files_external/l10n/sr.json +++ b/apps/files_external/l10n/sr.json @@ -35,7 +35,7 @@ "SFTP with secret key login" : "СФТП са пријавом помоћу тајног кључа", "Share" : "Дели", "Domain" : "Домен", - "SMB / CIFS using OC login" : "СМБ/ЦИФС користећи оунКлауд пријаву", + "SMB / CIFS using OC login" : "СМБ/ЦИФС користећи owncloud.online пријаву", "Username as share" : "Корисничко име као дељење", "Note: " : "Напомена: ", "Note: The cURL support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it." : "Напомена: cURL подршка за ПХП није омогућена или инсталирана. Монтирање %s није могуће. Затражите од вашег администратора система да је инсталира.", diff --git a/apps/files_external/l10n/ug.js b/apps/files_external/l10n/ug.js index 8d69caab74..9fb8a5bd4f 100644 --- a/apps/files_external/l10n/ug.js +++ b/apps/files_external/l10n/ug.js @@ -32,7 +32,7 @@ OC.L10N.register( "Error getting OAuth2 URL for " : "ئۈچۈن OAuth2 URL نى ئېلىشتا خاتالىق", "Empty response from the server" : "مۇلازىمېتىردىن قۇرۇق جاۋاب", "Couldn't access. Please logout and login to activate this mount point" : "زىيارەت قىلالمىدى. بۇ قاچىلاش نۇقتىسىنى ئاكتىپلاش ئۈچۈن تىزىملىتىپ كىرىڭ", - "Couldn't get the information from the remote server: {code} {type}" : "OwnCloud مۇلازىمېتىرىدىن ئۇچۇرغا ئېرىشەلمىدى: {code} {type}", + "Couldn't get the information from the remote server: {code} {type}" : "owncloud.online مۇلازىمېتىرىدىن ئۇچۇرغا ئېرىشەلمىدى: {code} {type}", "Couldn't get the list of external mount points: {type}" : "سىرتقى ئورنىتىش نۇقتىلىرىنىڭ تىزىملىكىنى ئالالمىدى: {تىپ}", "There was an error with message: " : "ئۇچۇردا خاتالىق كۆرۈلدى:", "External mount error" : "سىرتقى ئورنىتىش خاتالىقى", diff --git a/apps/files_external/l10n/ug.json b/apps/files_external/l10n/ug.json index db8b478f6b..bce0996d75 100644 --- a/apps/files_external/l10n/ug.json +++ b/apps/files_external/l10n/ug.json @@ -30,7 +30,7 @@ "Error getting OAuth2 URL for " : "ئۈچۈن OAuth2 URL نى ئېلىشتا خاتالىق", "Empty response from the server" : "مۇلازىمېتىردىن قۇرۇق جاۋاب", "Couldn't access. Please logout and login to activate this mount point" : "زىيارەت قىلالمىدى. بۇ قاچىلاش نۇقتىسىنى ئاكتىپلاش ئۈچۈن تىزىملىتىپ كىرىڭ", - "Couldn't get the information from the remote server: {code} {type}" : "OwnCloud مۇلازىمېتىرىدىن ئۇچۇرغا ئېرىشەلمىدى: {code} {type}", + "Couldn't get the information from the remote server: {code} {type}" : "owncloud.online مۇلازىمېتىرىدىن ئۇچۇرغا ئېرىشەلمىدى: {code} {type}", "Couldn't get the list of external mount points: {type}" : "سىرتقى ئورنىتىش نۇقتىلىرىنىڭ تىزىملىكىنى ئالالمىدى: {تىپ}", "There was an error with message: " : "ئۇچۇردا خاتالىق كۆرۈلدى:", "External mount error" : "سىرتقى ئورنىتىش خاتالىقى", diff --git a/apps/files_sharing/l10n/de.js b/apps/files_sharing/l10n/de.js index c3bb264b97..ee662923fe 100644 --- a/apps/files_sharing/l10n/de.js +++ b/apps/files_sharing/l10n/de.js @@ -34,8 +34,8 @@ OC.L10N.register( "Add to another cloud" : "Zu einer anderen Cloud hinzufügen", "Enter the server address to add the content to" : "Gib die Serveradresse ein, zu der der Inhalt hinzugefügt werden soll", "Add" : "Hinzufügen", - "No compatible server installation found at {remote}" : "Keine OwnCloud-Installation (7 oder höher) auf {remote} gefunden", - "Invalid server url" : "Ungültige OwnCloud-URL", + "No compatible server installation found at {remote}" : "Keine owncloud.online-Installation auf {remote} gefunden", + "Invalid server url" : "Ungültige owncloud.online-URL", "Share" : "Teilen", "Create public link" : "Öffentlichen Link erstellen", "Create and copy public link" : "Öffentlichen Link erstellen und kopieren", diff --git a/apps/files_sharing/l10n/de.json b/apps/files_sharing/l10n/de.json index b8a1e02bc6..abb881ba16 100644 --- a/apps/files_sharing/l10n/de.json +++ b/apps/files_sharing/l10n/de.json @@ -32,8 +32,8 @@ "Add to another cloud" : "Zu einer anderen Cloud hinzufügen", "Enter the server address to add the content to" : "Gib die Serveradresse ein, zu der der Inhalt hinzugefügt werden soll", "Add" : "Hinzufügen", - "No compatible server installation found at {remote}" : "Keine OwnCloud-Installation (7 oder höher) auf {remote} gefunden", - "Invalid server url" : "Ungültige OwnCloud-URL", + "No compatible server installation found at {remote}" : "Keine owncloud.online-Installation auf {remote} gefunden", + "Invalid server url" : "Ungültige owncloud.online-URL", "Share" : "Teilen", "Create public link" : "Öffentlichen Link erstellen", "Create and copy public link" : "Öffentlichen Link erstellen und kopieren", diff --git a/apps/files_sharing/l10n/de_CH.js b/apps/files_sharing/l10n/de_CH.js index ca6fbcd36a..0084092593 100644 --- a/apps/files_sharing/l10n/de_CH.js +++ b/apps/files_sharing/l10n/de_CH.js @@ -24,8 +24,8 @@ OC.L10N.register( "Cancel" : "Abbrechen", "You can upload into this folder" : "Du kannst in diesen Ordner hochladen", "Add" : "Hinzufügen", - "No compatible server installation found at {remote}" : "Keine OwnCloud-Installation (7 oder höher) auf {remote} gefunden", - "Invalid server url" : "Ungültige OwnCloud-URL", + "No compatible server installation found at {remote}" : "Keine owncloud.online-Installation auf {remote} gefunden", + "Invalid server url" : "Ungültige owncloud.online-URL", "Share" : "Teilen", "Create public link" : "Öffentlichen Link erstellen", "No expiration date set" : "Kein Ablaufdatum angegeben", diff --git a/apps/files_sharing/l10n/de_CH.json b/apps/files_sharing/l10n/de_CH.json index f345361ca4..6cc2e2d48e 100644 --- a/apps/files_sharing/l10n/de_CH.json +++ b/apps/files_sharing/l10n/de_CH.json @@ -22,8 +22,8 @@ "Cancel" : "Abbrechen", "You can upload into this folder" : "Du kannst in diesen Ordner hochladen", "Add" : "Hinzufügen", - "No compatible server installation found at {remote}" : "Keine OwnCloud-Installation (7 oder höher) auf {remote} gefunden", - "Invalid server url" : "Ungültige OwnCloud-URL", + "No compatible server installation found at {remote}" : "Keine owncloud.online-Installation auf {remote} gefunden", + "Invalid server url" : "Ungültige owncloud.online-URL", "Share" : "Teilen", "Create public link" : "Öffentlichen Link erstellen", "No expiration date set" : "Kein Ablaufdatum angegeben", diff --git a/apps/files_sharing/l10n/de_DE.js b/apps/files_sharing/l10n/de_DE.js index 0aef64a131..fb59435ff2 100644 --- a/apps/files_sharing/l10n/de_DE.js +++ b/apps/files_sharing/l10n/de_DE.js @@ -34,8 +34,8 @@ OC.L10N.register( "Add to another cloud" : "Zu einer anderen Cloud hinzufügen", "Enter the server address to add the content to" : "Geben Sie die Serveradresse ein, zu der der Inhalt hinzugefügt werden soll", "Add" : "Hinzufügen", - "No compatible server installation found at {remote}" : "Keine OwnCloud-Installation (7 oder höher) auf {remote} gefunden", - "Invalid server url" : "Ungültige OwnCloud-Adresse", + "No compatible server installation found at {remote}" : "Keine owncloud.online-Installation auf {remote} gefunden", + "Invalid server url" : "Ungültige owncloud.online-Adresse", "Share" : "Teilen", "Create public link" : "Öffentlichen Link erstellen", "Create and copy public link" : "Öffentlichen Link erstellen und kopieren", diff --git a/apps/files_sharing/l10n/de_DE.json b/apps/files_sharing/l10n/de_DE.json index 426e9b605a..04c3037db9 100644 --- a/apps/files_sharing/l10n/de_DE.json +++ b/apps/files_sharing/l10n/de_DE.json @@ -32,8 +32,8 @@ "Add to another cloud" : "Zu einer anderen Cloud hinzufügen", "Enter the server address to add the content to" : "Geben Sie die Serveradresse ein, zu der der Inhalt hinzugefügt werden soll", "Add" : "Hinzufügen", - "No compatible server installation found at {remote}" : "Keine OwnCloud-Installation (7 oder höher) auf {remote} gefunden", - "Invalid server url" : "Ungültige OwnCloud-Adresse", + "No compatible server installation found at {remote}" : "Keine owncloud.online-Installation auf {remote} gefunden", + "Invalid server url" : "Ungültige owncloud.online-Adresse", "Share" : "Teilen", "Create public link" : "Öffentlichen Link erstellen", "Create and copy public link" : "Öffentlichen Link erstellen und kopieren", diff --git a/apps/files_sharing/l10n/es_AR.js b/apps/files_sharing/l10n/es_AR.js index e39b9982fe..416c84835e 100644 --- a/apps/files_sharing/l10n/es_AR.js +++ b/apps/files_sharing/l10n/es_AR.js @@ -8,7 +8,7 @@ OC.L10N.register( "Uploading..." : "Subiendo...", "Cancel" : "Cancelar", "Add" : "Agregar", - "Invalid server url" : "La url de OwnCloud es inválida", + "Invalid server url" : "La url de owncloud.online es inválida", "Share" : "Compartir", "Declined" : "Denegado", "Pending" : "Pendientes", diff --git a/apps/files_sharing/l10n/es_AR.json b/apps/files_sharing/l10n/es_AR.json index cd1e7e77b0..ff92f04f7c 100644 --- a/apps/files_sharing/l10n/es_AR.json +++ b/apps/files_sharing/l10n/es_AR.json @@ -6,7 +6,7 @@ "Uploading..." : "Subiendo...", "Cancel" : "Cancelar", "Add" : "Agregar", - "Invalid server url" : "La url de OwnCloud es inválida", + "Invalid server url" : "La url de owncloud.online es inválida", "Share" : "Compartir", "Declined" : "Denegado", "Pending" : "Pendientes", diff --git a/apps/files_sharing/l10n/nn_NO.js b/apps/files_sharing/l10n/nn_NO.js index 61d56a2809..2a8c14bcf2 100644 --- a/apps/files_sharing/l10n/nn_NO.js +++ b/apps/files_sharing/l10n/nn_NO.js @@ -14,7 +14,7 @@ OC.L10N.register( "Cancel" : "Avbryt", "You can upload into this folder" : "Du kan lste opp til denne mappa", "Add" : "Legg til", - "Invalid server url" : "Ugyldig ownClud url", + "Invalid server url" : "Ugyldig owncloud.online url", "Share" : "Del", "Create public link" : "Opprett offentleg lenke", "No expiration date set" : "Ingen dato for utløp er sett", diff --git a/apps/files_sharing/l10n/nn_NO.json b/apps/files_sharing/l10n/nn_NO.json index c47927a464..cad44f93e8 100644 --- a/apps/files_sharing/l10n/nn_NO.json +++ b/apps/files_sharing/l10n/nn_NO.json @@ -12,7 +12,7 @@ "Cancel" : "Avbryt", "You can upload into this folder" : "Du kan lste opp til denne mappa", "Add" : "Legg til", - "Invalid server url" : "Ugyldig ownClud url", + "Invalid server url" : "Ugyldig owncloud.online url", "Share" : "Del", "Create public link" : "Opprett offentleg lenke", "No expiration date set" : "Ingen dato for utløp er sett", diff --git a/apps/files_sharing/l10n/pt_PT.js b/apps/files_sharing/l10n/pt_PT.js index c306ef0161..3223bbef26 100644 --- a/apps/files_sharing/l10n/pt_PT.js +++ b/apps/files_sharing/l10n/pt_PT.js @@ -34,7 +34,7 @@ OC.L10N.register( "Add to another cloud" : "Adicionar a outra nuvem ", "Enter the server address to add the content to" : "Insira o endereço do servidor para adicionar o conteúdo ", "Add" : "Adicionar", - "No compatible server installation found at {remote}" : "Não foi encontrada nenhuma instalação OwnCloud (7 ou superior) em {remote}", + "No compatible server installation found at {remote}" : "Não foi encontrada nenhuma instalação owncloud.online em {remote}", "Invalid server url" : "Url da owncloud.online inválido", "Share" : "Partilhar", "Create public link" : "Criar ligação pública", diff --git a/apps/files_sharing/l10n/pt_PT.json b/apps/files_sharing/l10n/pt_PT.json index f8c3d327d3..992bc063e7 100644 --- a/apps/files_sharing/l10n/pt_PT.json +++ b/apps/files_sharing/l10n/pt_PT.json @@ -32,7 +32,7 @@ "Add to another cloud" : "Adicionar a outra nuvem ", "Enter the server address to add the content to" : "Insira o endereço do servidor para adicionar o conteúdo ", "Add" : "Adicionar", - "No compatible server installation found at {remote}" : "Não foi encontrada nenhuma instalação OwnCloud (7 ou superior) em {remote}", + "No compatible server installation found at {remote}" : "Não foi encontrada nenhuma instalação owncloud.online em {remote}", "Invalid server url" : "Url da owncloud.online inválido", "Share" : "Partilhar", "Create public link" : "Criar ligação pública", diff --git a/apps/files_sharing/l10n/sr.js b/apps/files_sharing/l10n/sr.js index 124cebcbc3..5bfa49a124 100644 --- a/apps/files_sharing/l10n/sr.js +++ b/apps/files_sharing/l10n/sr.js @@ -17,8 +17,8 @@ OC.L10N.register( "Cancel" : "Одустани", "You can upload into this folder" : "Можете да отпремате у ову фасциклу", "Add" : "Додај", - "No compatible server installation found at {remote}" : "Нема оунКлауд инсталације верзије 7 или више на {remote}", - "Invalid server url" : "Неисправан оунКлауд УРЛ", + "No compatible server installation found at {remote}" : "Нема owncloud.online инсталације на {remote}", + "Invalid server url" : "Неисправан owncloud.online УРЛ", "Share" : "Дељење", "Pending" : "На чекању", "Shared by" : "Дели", diff --git a/apps/files_sharing/l10n/sr.json b/apps/files_sharing/l10n/sr.json index 9238eea35a..2ea8af1591 100644 --- a/apps/files_sharing/l10n/sr.json +++ b/apps/files_sharing/l10n/sr.json @@ -15,8 +15,8 @@ "Cancel" : "Одустани", "You can upload into this folder" : "Можете да отпремате у ову фасциклу", "Add" : "Додај", - "No compatible server installation found at {remote}" : "Нема оунКлауд инсталације верзије 7 или више на {remote}", - "Invalid server url" : "Неисправан оунКлауд УРЛ", + "No compatible server installation found at {remote}" : "Нема owncloud.online инсталације на {remote}", + "Invalid server url" : "Неисправан owncloud.online УРЛ", "Share" : "Дељење", "Pending" : "На чекању", "Shared by" : "Дели", diff --git a/apps/files_sharing/l10n/sv.js b/apps/files_sharing/l10n/sv.js index 1517ffaca6..39be038d60 100644 --- a/apps/files_sharing/l10n/sv.js +++ b/apps/files_sharing/l10n/sv.js @@ -21,7 +21,7 @@ OC.L10N.register( "Cancel" : "Avbryt", "You can upload into this folder" : "Du kan ladda upp i denna map", "Add" : "Lägg till", - "No compatible server installation found at {remote}" : "Ingen OwnCloud Installation (7 eller senare) funnen på {remote}", + "No compatible server installation found at {remote}" : "Ingen owncloud.online-installation funnen på {remote}", "Invalid server url" : "Felaktig owncloud.online url", "Share" : "Dela", "Create public link" : "Skapa publik länk", diff --git a/apps/files_sharing/l10n/sv.json b/apps/files_sharing/l10n/sv.json index 08326714c1..f518a9fa00 100644 --- a/apps/files_sharing/l10n/sv.json +++ b/apps/files_sharing/l10n/sv.json @@ -19,7 +19,7 @@ "Cancel" : "Avbryt", "You can upload into this folder" : "Du kan ladda upp i denna map", "Add" : "Lägg till", - "No compatible server installation found at {remote}" : "Ingen OwnCloud Installation (7 eller senare) funnen på {remote}", + "No compatible server installation found at {remote}" : "Ingen owncloud.online-installation funnen på {remote}", "Invalid server url" : "Felaktig owncloud.online url", "Share" : "Dela", "Create public link" : "Skapa publik länk", diff --git a/apps/files_sharing/l10n/uk.js b/apps/files_sharing/l10n/uk.js index 45b2dde7a1..f143178653 100644 --- a/apps/files_sharing/l10n/uk.js +++ b/apps/files_sharing/l10n/uk.js @@ -17,7 +17,7 @@ OC.L10N.register( "Cancel" : "Відмінити", "You can upload into this folder" : "Ви можете завантажити до цієї теки", "Add" : "Додати", - "No compatible server installation found at {remote}" : "Немає установленого OwnCloud (7 або вище), можна знайти на {remote}", + "No compatible server installation found at {remote}" : "Немає установленого owncloud.online, можна знайти на {remote}", "Invalid server url" : "Невірний owncloud.online URL", "Share" : "Поділитися", "Create public link" : "Створити публічне посилання", diff --git a/apps/files_sharing/l10n/uk.json b/apps/files_sharing/l10n/uk.json index 9da7c3e2ae..b0ab18faa2 100644 --- a/apps/files_sharing/l10n/uk.json +++ b/apps/files_sharing/l10n/uk.json @@ -15,7 +15,7 @@ "Cancel" : "Відмінити", "You can upload into this folder" : "Ви можете завантажити до цієї теки", "Add" : "Додати", - "No compatible server installation found at {remote}" : "Немає установленого OwnCloud (7 або вище), можна знайти на {remote}", + "No compatible server installation found at {remote}" : "Немає установленого owncloud.online, можна знайти на {remote}", "Invalid server url" : "Невірний owncloud.online URL", "Share" : "Поділитися", "Create public link" : "Створити публічне посилання", diff --git a/apps/updatenotification/l10n/da.js b/apps/updatenotification/l10n/da.js index b197524841..8df18589db 100644 --- a/apps/updatenotification/l10n/da.js +++ b/apps/updatenotification/l10n/da.js @@ -13,6 +13,6 @@ OC.L10N.register( "Update channel:" : "Opdatér kanal:", "You can always update to a newer version / experimental channel. But you can never downgrade to a more stable channel." : "Du kan altid opdatere til en nyere version / eksperimentel kanal. Men du kan aldrig nedgradere til en mere stabil kanal", "Notify members of the following groups about available updates:" : "Underret medlemmer af følgende grupper, om tilgængelige opdateringer:", - "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Kun meddelelse til app opdateringer er tilgængelige, fordi den valgte opdateringskanal for OwnCloud selv, ikke tillader meddelelser." + "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Kun meddelelse til app opdateringer er tilgængelige, fordi den valgte opdateringskanal for owncloud.online selv, ikke tillader meddelelser." }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/updatenotification/l10n/da.json b/apps/updatenotification/l10n/da.json index 67f66c3ef7..f27556504a 100644 --- a/apps/updatenotification/l10n/da.json +++ b/apps/updatenotification/l10n/da.json @@ -11,6 +11,6 @@ "Update channel:" : "Opdatér kanal:", "You can always update to a newer version / experimental channel. But you can never downgrade to a more stable channel." : "Du kan altid opdatere til en nyere version / eksperimentel kanal. Men du kan aldrig nedgradere til en mere stabil kanal", "Notify members of the following groups about available updates:" : "Underret medlemmer af følgende grupper, om tilgængelige opdateringer:", - "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Kun meddelelse til app opdateringer er tilgængelige, fordi den valgte opdateringskanal for OwnCloud selv, ikke tillader meddelelser." + "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Kun meddelelse til app opdateringer er tilgængelige, fordi den valgte opdateringskanal for owncloud.online selv, ikke tillader meddelelser." },"pluralForm" :"nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/apps/updatenotification/l10n/es_AR.js b/apps/updatenotification/l10n/es_AR.js index e178354ae6..fd0d37278e 100644 --- a/apps/updatenotification/l10n/es_AR.js +++ b/apps/updatenotification/l10n/es_AR.js @@ -4,7 +4,7 @@ OC.L10N.register( "Update notifications" : "Actualizar notificaciones", "{version} is available. Get more information on how to update." : "{version} está disponible. Obtenga más información sobre cómo actualizar.", "Updated channel" : "Canal actualizado", - "owncloud.online core" : "núcleo wnCloud", + "owncloud.online core" : "núcleo de owncloud.online", "Update for %1$s to version %2$s is available." : "Está disponible la actualización de versión %1$s a versión%2$s.", "Updater" : "Actualizador", "A new version is available: %s" : "Una nueva versión esta disponible: %s", diff --git a/apps/updatenotification/l10n/es_AR.json b/apps/updatenotification/l10n/es_AR.json index ce982cc95e..0273d2fea1 100644 --- a/apps/updatenotification/l10n/es_AR.json +++ b/apps/updatenotification/l10n/es_AR.json @@ -2,7 +2,7 @@ "Update notifications" : "Actualizar notificaciones", "{version} is available. Get more information on how to update." : "{version} está disponible. Obtenga más información sobre cómo actualizar.", "Updated channel" : "Canal actualizado", - "owncloud.online core" : "núcleo wnCloud", + "owncloud.online core" : "núcleo de owncloud.online", "Update for %1$s to version %2$s is available." : "Está disponible la actualización de versión %1$s a versión%2$s.", "Updater" : "Actualizador", "A new version is available: %s" : "Una nueva versión esta disponible: %s", diff --git a/apps/updatenotification/l10n/pt_BR.js b/apps/updatenotification/l10n/pt_BR.js index 05769728e9..7304ebbdaf 100644 --- a/apps/updatenotification/l10n/pt_BR.js +++ b/apps/updatenotification/l10n/pt_BR.js @@ -14,6 +14,6 @@ OC.L10N.register( "You can always update to a newer version / experimental channel. But you can never downgrade to a more stable channel." : "Você pode sempre atualizar para uma versão / canal experimental mais recente. Mas você nunca pode voltar para um canal mais estável.", "Notify members of the following groups about available updates:" : "Notificar membros dos seguintes grupos sobre atualizações disponíveis.", "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Somente notificação para atualizações de aplicativos estão disponíveis, porque o canal de atualização selecionada para owncloud.online não permite notificações.", - "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "O atualizador online foi desativado automaticamente porque você está executando uma edição OwnCloud Enterprise. Atualize sua instância owncloud.online manualmente." + "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "O atualizador online foi desativado automaticamente porque você está executando uma edição Enterprise. Atualize sua instância owncloud.online manualmente." }, "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/updatenotification/l10n/pt_BR.json b/apps/updatenotification/l10n/pt_BR.json index 09935828f6..9ce0c4c440 100644 --- a/apps/updatenotification/l10n/pt_BR.json +++ b/apps/updatenotification/l10n/pt_BR.json @@ -12,6 +12,6 @@ "You can always update to a newer version / experimental channel. But you can never downgrade to a more stable channel." : "Você pode sempre atualizar para uma versão / canal experimental mais recente. Mas você nunca pode voltar para um canal mais estável.", "Notify members of the following groups about available updates:" : "Notificar membros dos seguintes grupos sobre atualizações disponíveis.", "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Somente notificação para atualizações de aplicativos estão disponíveis, porque o canal de atualização selecionada para owncloud.online não permite notificações.", - "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "O atualizador online foi desativado automaticamente porque você está executando uma edição OwnCloud Enterprise. Atualize sua instância owncloud.online manualmente." + "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "O atualizador online foi desativado automaticamente porque você está executando uma edição Enterprise. Atualize sua instância owncloud.online manualmente." },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/apps/updatenotification/l10n/pt_PT.js b/apps/updatenotification/l10n/pt_PT.js index 6505b7e815..9edd06f57c 100644 --- a/apps/updatenotification/l10n/pt_PT.js +++ b/apps/updatenotification/l10n/pt_PT.js @@ -14,6 +14,6 @@ OC.L10N.register( "You can always update to a newer version / experimental channel. But you can never downgrade to a more stable channel." : "Pode atualizar sempre para uma nova versão / canal experimental. Mas nunca poderá voltar para um canal mais estável.", "Notify members of the following groups about available updates:" : "Notifique os membros dos seguintes grupos sobre as atualizações disponíveis:", "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Só estão disponíveis notificações para as atualizações de aplicações, porque o canal de atualização selecionado para a própria owncloud.online não permite notificações.", - "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "O atualizador online foi desativado automaticamente porque você está executando uma edição OwnCloud Enterprise. Atualize sua instância owncloud.online manualmente." + "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "O atualizador online foi desativado automaticamente porque você está executando uma edição Enterprise. Atualize sua instância owncloud.online manualmente." }, "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/updatenotification/l10n/pt_PT.json b/apps/updatenotification/l10n/pt_PT.json index 9ff79fc58d..4411a9c0e9 100644 --- a/apps/updatenotification/l10n/pt_PT.json +++ b/apps/updatenotification/l10n/pt_PT.json @@ -12,6 +12,6 @@ "You can always update to a newer version / experimental channel. But you can never downgrade to a more stable channel." : "Pode atualizar sempre para uma nova versão / canal experimental. Mas nunca poderá voltar para um canal mais estável.", "Notify members of the following groups about available updates:" : "Notifique os membros dos seguintes grupos sobre as atualizações disponíveis:", "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Só estão disponíveis notificações para as atualizações de aplicações, porque o canal de atualização selecionado para a própria owncloud.online não permite notificações.", - "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "O atualizador online foi desativado automaticamente porque você está executando uma edição OwnCloud Enterprise. Atualize sua instância owncloud.online manualmente." + "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "O atualizador online foi desativado automaticamente porque você está executando uma edição Enterprise. Atualize sua instância owncloud.online manualmente." },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" } \ No newline at end of file diff --git a/apps/updatenotification/l10n/ug.js b/apps/updatenotification/l10n/ug.js index 86c57da10f..54a0952542 100644 --- a/apps/updatenotification/l10n/ug.js +++ b/apps/updatenotification/l10n/ug.js @@ -14,6 +14,6 @@ OC.L10N.register( "You can always update to a newer version / experimental channel. But you can never downgrade to a more stable channel." : "سىز ھەمىشە يېڭى نەشرى / تەجرىبە قانىلىغا يېڭىلىيالايسىز. ئەمما سىز ھەرگىزمۇ تېخىمۇ مۇقىم قانالغا چۈشۈرەلمەيسىز.", "Notify members of the following groups about available updates:" : "تۆۋەندىكى گۇرۇپپىلارنىڭ ئەزالىرىغا يېڭىلانمىلارنى ئۇقتۇرۇڭ:", "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "پەقەت ئەپ يېڭىلاشقا مۇناسىۋەتلىك ئۇقتۇرۇشلارلا بار ، چۈنكى owncloud.online نىڭ ئۆزى تاللىغان يېڭىلاش قانىلى ئۇقتۇرۇشقا يول قويمايدۇ.", - "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "سىز ھازىر ئۆزىڭىزنىڭ Cloud كارخانا نەشرىنى ئىجرا قىلىۋاتقان بولغاچقا ، تور يېڭىلاش ماشىنىسى ئاپتوماتىك چەكلەنگەن. ئۆزىڭىزنىڭ Cloud ئۈلگىسىنى قولدا يېڭىلاڭ." + "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "سىز ھازىر ئۆزىڭىزنىڭ كارخانا نەشرىنى ئىجرا قىلىۋاتقان بولغاچقا ، تور يېڭىلاش ماشىنىسى ئاپتوماتىك چەكلەنگەن. ئۆزىڭىزنىڭ ئۈلگىسىنى قولدا يېڭىلاڭ." }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/updatenotification/l10n/ug.json b/apps/updatenotification/l10n/ug.json index e853a83c08..d212e9c84f 100644 --- a/apps/updatenotification/l10n/ug.json +++ b/apps/updatenotification/l10n/ug.json @@ -12,6 +12,6 @@ "You can always update to a newer version / experimental channel. But you can never downgrade to a more stable channel." : "سىز ھەمىشە يېڭى نەشرى / تەجرىبە قانىلىغا يېڭىلىيالايسىز. ئەمما سىز ھەرگىزمۇ تېخىمۇ مۇقىم قانالغا چۈشۈرەلمەيسىز.", "Notify members of the following groups about available updates:" : "تۆۋەندىكى گۇرۇپپىلارنىڭ ئەزالىرىغا يېڭىلانمىلارنى ئۇقتۇرۇڭ:", "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "پەقەت ئەپ يېڭىلاشقا مۇناسىۋەتلىك ئۇقتۇرۇشلارلا بار ، چۈنكى owncloud.online نىڭ ئۆزى تاللىغان يېڭىلاش قانىلى ئۇقتۇرۇشقا يول قويمايدۇ.", - "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "سىز ھازىر ئۆزىڭىزنىڭ Cloud كارخانا نەشرىنى ئىجرا قىلىۋاتقان بولغاچقا ، تور يېڭىلاش ماشىنىسى ئاپتوماتىك چەكلەنگەن. ئۆزىڭىزنىڭ Cloud ئۈلگىسىنى قولدا يېڭىلاڭ." + "Online updater has been automatically disabled as you are currently running an Enterprise edition. Please update your instance manually." : "سىز ھازىر ئۆزىڭىزنىڭ كارخانا نەشرىنى ئىجرا قىلىۋاتقان بولغاچقا ، تور يېڭىلاش ماشىنىسى ئاپتوماتىك چەكلەنگەن. ئۆزىڭىزنىڭ ئۈلگىسىنى قولدا يېڭىلاڭ." },"pluralForm" :"nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/apps/updatenotification/l10n/uk.js b/apps/updatenotification/l10n/uk.js index fb4aeaab5a..00a554f909 100644 --- a/apps/updatenotification/l10n/uk.js +++ b/apps/updatenotification/l10n/uk.js @@ -13,6 +13,6 @@ OC.L10N.register( "Update channel:" : "Канал оновлення:", "You can always update to a newer version / experimental channel. But you can never downgrade to a more stable channel." : "Ви завжди зможете оновити owncloud.online до новішої версії або перейти на тестову версію. Але ви не зможете встановити старішу версію або перейти на стабільний канал.", "Notify members of the following groups about available updates:" : "Сповіщати учасників даної групи про доступні оновлення.", - "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Доступні тільки повідомлення для оновлення додатків, оскільки обраний канал поновлення не дозволяє повідомлень для самого OwnCloud." + "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Доступні тільки повідомлення для оновлення додатків, оскільки обраний канал поновлення не дозволяє повідомлень для самого owncloud.online." }, "nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"); diff --git a/apps/updatenotification/l10n/uk.json b/apps/updatenotification/l10n/uk.json index 0058e96b30..1cb57c283d 100644 --- a/apps/updatenotification/l10n/uk.json +++ b/apps/updatenotification/l10n/uk.json @@ -11,6 +11,6 @@ "Update channel:" : "Канал оновлення:", "You can always update to a newer version / experimental channel. But you can never downgrade to a more stable channel." : "Ви завжди зможете оновити owncloud.online до новішої версії або перейти на тестову версію. Але ви не зможете встановити старішу версію або перейти на стабільний канал.", "Notify members of the following groups about available updates:" : "Сповіщати учасників даної групи про доступні оновлення.", - "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Доступні тільки повідомлення для оновлення додатків, оскільки обраний канал поновлення не дозволяє повідомлень для самого OwnCloud." + "Only notification for app updates are available, because the selected update channel for the server itself does not allow notifications." : "Доступні тільки повідомлення для оновлення додатків, оскільки обраний канал поновлення не дозволяє повідомлень для самого owncloud.online." },"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);" } \ No newline at end of file diff --git a/changelog/unreleased/l10n-current-product-name b/changelog/unreleased/l10n-current-product-name new file mode 100644 index 0000000000..07cd4f1043 --- /dev/null +++ b/changelog/unreleased/l10n-current-product-name @@ -0,0 +1,8 @@ +Bugfix: Translations name owncloud.online instead of the old product + +Several translations of the core and the bundled apps still showed the old +product name, partly misspelt or transliterated, for example the German notice +that the instance is in single user mode and the hints about trusted and +federated servers. They now name owncloud.online, in the .json and in the .js +file of each language. Example paths in the sharing hint and placeholders are +unchanged. diff --git a/core/l10n/de.js b/core/l10n/de.js index e37c992040..b4d1e7c040 100644 --- a/core/l10n/de.js +++ b/core/l10n/de.js @@ -371,7 +371,7 @@ OC.L10N.register( "Confirm password" : "Passwort bestätigen", "Reset password" : "Passwort zurücksetzen", "Personal note from the sender:
%s." : "Persönliche Notiz vom Absender:
%s.", - "This owncloud.online instance is currently in single user mode." : "Diese ownClound-Instanz befindet sich derzeit im Einzelbenutzermodus.", + "This owncloud.online instance is currently in single user mode." : "Diese owncloud.online-Instanz befindet sich derzeit im Einzelbenutzermodus.", "This means only administrators can use the instance." : "Dies bedeutet, dass diese Instanz nur von Administratoren genutzt werden kann.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Kontaktiere deinen Systemadministrator, wenn diese Meldung dauerhaft oder unerwartet erscheint.", "Thank you for your patience." : "Vielen Dank für deine Geduld.", diff --git a/core/l10n/de.json b/core/l10n/de.json index 7a2a41d5b4..05bcb33fce 100644 --- a/core/l10n/de.json +++ b/core/l10n/de.json @@ -369,7 +369,7 @@ "Confirm password" : "Passwort bestätigen", "Reset password" : "Passwort zurücksetzen", "Personal note from the sender:
%s." : "Persönliche Notiz vom Absender:
%s.", - "This owncloud.online instance is currently in single user mode." : "Diese ownClound-Instanz befindet sich derzeit im Einzelbenutzermodus.", + "This owncloud.online instance is currently in single user mode." : "Diese owncloud.online-Instanz befindet sich derzeit im Einzelbenutzermodus.", "This means only administrators can use the instance." : "Dies bedeutet, dass diese Instanz nur von Administratoren genutzt werden kann.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Kontaktiere deinen Systemadministrator, wenn diese Meldung dauerhaft oder unerwartet erscheint.", "Thank you for your patience." : "Vielen Dank für deine Geduld.", diff --git a/core/l10n/de_AT.js b/core/l10n/de_AT.js index f262175ce2..9493ae86aa 100644 --- a/core/l10n/de_AT.js +++ b/core/l10n/de_AT.js @@ -270,7 +270,7 @@ OC.L10N.register( "Use the following link to reset your password: {link}" : "Benutze den folgenden Link, um Dein Passwort zurückzusetzen: {link}", "New password" : "Neues Passwort", "Reset password" : "Passwort zurücksetzen", - "This owncloud.online instance is currently in single user mode." : "Diese ownClound-Instanz befindet sich derzeit im Einzelbenutzermodus.", + "This owncloud.online instance is currently in single user mode." : "Diese owncloud.online-Instanz befindet sich derzeit im Einzelbenutzermodus.", "This means only administrators can use the instance." : "Dies bedeutet, dass diese Instanz nur von Administratoren genutzt werden kann.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Kontaktiere Deinen Systemadministrator, wenn diese Meldung dauerhaft oder unerwartet erscheint.", "Thank you for your patience." : "Vielen Dank für Deine Geduld.", diff --git a/core/l10n/de_AT.json b/core/l10n/de_AT.json index 51cbfadae9..5235e9e097 100644 --- a/core/l10n/de_AT.json +++ b/core/l10n/de_AT.json @@ -268,7 +268,7 @@ "Use the following link to reset your password: {link}" : "Benutze den folgenden Link, um Dein Passwort zurückzusetzen: {link}", "New password" : "Neues Passwort", "Reset password" : "Passwort zurücksetzen", - "This owncloud.online instance is currently in single user mode." : "Diese ownClound-Instanz befindet sich derzeit im Einzelbenutzermodus.", + "This owncloud.online instance is currently in single user mode." : "Diese owncloud.online-Instanz befindet sich derzeit im Einzelbenutzermodus.", "This means only administrators can use the instance." : "Dies bedeutet, dass diese Instanz nur von Administratoren genutzt werden kann.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Kontaktiere Deinen Systemadministrator, wenn diese Meldung dauerhaft oder unerwartet erscheint.", "Thank you for your patience." : "Vielen Dank für Deine Geduld.", diff --git a/core/l10n/de_CH.js b/core/l10n/de_CH.js index c31460cacf..03cf4df936 100644 --- a/core/l10n/de_CH.js +++ b/core/l10n/de_CH.js @@ -357,7 +357,7 @@ OC.L10N.register( "Confirm password" : "Passwort bestätigen", "Reset password" : "Passwort zurücksetzen", "Personal note from the sender:
%s." : "Persönliche Notiz vom Absender:
%s.", - "This owncloud.online instance is currently in single user mode." : "Diese ownClound-Instanz befindet sich derzeit im Einzelbenutzermodus.", + "This owncloud.online instance is currently in single user mode." : "Diese owncloud.online-Instanz befindet sich derzeit im Einzelbenutzermodus.", "This means only administrators can use the instance." : "Dies bedeutet, dass diese Instanz nur von Administratoren genutzt werden kann.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Kontaktiere Deinen Systemadministrator, wenn diese Meldung dauerhaft oder unerwartet erscheint.", "Thank you for your patience." : "Vielen Dank für Deine Geduld.", diff --git a/core/l10n/de_CH.json b/core/l10n/de_CH.json index 4c14d734a8..bca7eb5cbb 100644 --- a/core/l10n/de_CH.json +++ b/core/l10n/de_CH.json @@ -355,7 +355,7 @@ "Confirm password" : "Passwort bestätigen", "Reset password" : "Passwort zurücksetzen", "Personal note from the sender:
%s." : "Persönliche Notiz vom Absender:
%s.", - "This owncloud.online instance is currently in single user mode." : "Diese ownClound-Instanz befindet sich derzeit im Einzelbenutzermodus.", + "This owncloud.online instance is currently in single user mode." : "Diese owncloud.online-Instanz befindet sich derzeit im Einzelbenutzermodus.", "This means only administrators can use the instance." : "Dies bedeutet, dass diese Instanz nur von Administratoren genutzt werden kann.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Kontaktiere Deinen Systemadministrator, wenn diese Meldung dauerhaft oder unerwartet erscheint.", "Thank you for your patience." : "Vielen Dank für Deine Geduld.", diff --git a/core/l10n/de_DE.js b/core/l10n/de_DE.js index 60c86e7608..3d98c3d319 100644 --- a/core/l10n/de_DE.js +++ b/core/l10n/de_DE.js @@ -371,7 +371,7 @@ OC.L10N.register( "Confirm password" : "Passwort bestätigen", "Reset password" : "Passwort zurücksetzen", "Personal note from the sender:
%s." : "Persönliche Notiz vom Absender:
%s.", - "This owncloud.online instance is currently in single user mode." : "Diese ownClound-Instanz befindet sich derzeit im Einzelbenutzermodus.", + "This owncloud.online instance is currently in single user mode." : "Diese owncloud.online-Instanz befindet sich derzeit im Einzelbenutzermodus.", "This means only administrators can use the instance." : "Das bedeutet, dass diese Instanz nur von Administratoren benutzt werden kann.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Kontaktieren Sie Ihren Systemadministration, wenn diese Meldung dauerhaft oder unerwartet erscheint.", "Thank you for your patience." : "Vielen Dank für Ihre Geduld.", diff --git a/core/l10n/de_DE.json b/core/l10n/de_DE.json index 2d0a288265..84d25fc92d 100644 --- a/core/l10n/de_DE.json +++ b/core/l10n/de_DE.json @@ -369,7 +369,7 @@ "Confirm password" : "Passwort bestätigen", "Reset password" : "Passwort zurücksetzen", "Personal note from the sender:
%s." : "Persönliche Notiz vom Absender:
%s.", - "This owncloud.online instance is currently in single user mode." : "Diese ownClound-Instanz befindet sich derzeit im Einzelbenutzermodus.", + "This owncloud.online instance is currently in single user mode." : "Diese owncloud.online-Instanz befindet sich derzeit im Einzelbenutzermodus.", "This means only administrators can use the instance." : "Das bedeutet, dass diese Instanz nur von Administratoren benutzt werden kann.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Kontaktieren Sie Ihren Systemadministration, wenn diese Meldung dauerhaft oder unerwartet erscheint.", "Thank you for your patience." : "Vielen Dank für Ihre Geduld.", diff --git a/core/l10n/eu.js b/core/l10n/eu.js index e40ea80d21..2a11f00dcf 100644 --- a/core/l10n/eu.js +++ b/core/l10n/eu.js @@ -183,7 +183,7 @@ OC.L10N.register( "Group" : "Taldea", "User and Groups" : "Erabiltzaile eta Taldeak", "Public Links" : "Esteka Publikoak", - "Share with people on other servers using the syntax username@example.com/owncloud" : "Partekatu beste ownClud-en jendearekin sintaxi honekin erabiltzaleizena@adibidea.com/owncloud", + "Share with people on other servers using the syntax username@example.com/owncloud" : "Partekatu beste owncloud.online-en jendearekin sintaxi honekin erabiltzaleizena@adibidea.com/owncloud", "Rename" : "Berrizendatu", "Delete" : "Ezabatu", "Collaborative tags" : "Kolaboraziorako etiketak", diff --git a/core/l10n/eu.json b/core/l10n/eu.json index 56a32dec0b..6924ea5f96 100644 --- a/core/l10n/eu.json +++ b/core/l10n/eu.json @@ -181,7 +181,7 @@ "Group" : "Taldea", "User and Groups" : "Erabiltzaile eta Taldeak", "Public Links" : "Esteka Publikoak", - "Share with people on other servers using the syntax username@example.com/owncloud" : "Partekatu beste ownClud-en jendearekin sintaxi honekin erabiltzaleizena@adibidea.com/owncloud", + "Share with people on other servers using the syntax username@example.com/owncloud" : "Partekatu beste owncloud.online-en jendearekin sintaxi honekin erabiltzaleizena@adibidea.com/owncloud", "Rename" : "Berrizendatu", "Delete" : "Ezabatu", "Collaborative tags" : "Kolaboraziorako etiketak", diff --git a/core/l10n/mk.js b/core/l10n/mk.js index 51ec9e31ef..45f25fd3ee 100644 --- a/core/l10n/mk.js +++ b/core/l10n/mk.js @@ -142,7 +142,7 @@ OC.L10N.register( "No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our documentation." : "Не е конфигуриран кеш меморија. За да ги подобрите вашите перформанси, конфигурирајте memcache ако е достапно. Повеќе информации може да најдете во нашата документација .", "/dev/urandom is not readable by PHP which is highly discouraged for security reasons. Further information can be found in our documentation." : "/dev/urandom не може да се чита од PHP, што е многу обесхрабрен поради безбедносни причини. Повеќе информации може да најдете во нашата документација .", "You are currently running PHP {version}. We encourage you to upgrade your PHP version to take advantage of performance and security updates provided by the PHP Group as soon as your distribution supports it." : "Во моментов работиш со PHP {верзија}. Ве охрабруваме да ја надградите Вашата верзија на PHP за да ги искористите ажурирањата за ефикасноста и сигурноста обезбедени од страна на PHP Групата веднаш штом вашата дистрибуција го поддржува.", - "The reverse proxy headers configuration is incorrect, or you are accessing the server from a trusted proxy. If you are not accessing the server from a trusted proxy, this is a security issue and can allow an attacker to spoof their IP address as visible to the server. Further information can be found in our documentation." : "Конфигурација на заглавјето за прокси е неточна, или пристапувате до OwnCloud преку доверлив прокси. Ако не пристапувате до owncloud.online од доверлив прокси, ова е безбедносно прашање и може да дозволите напаѓачот да ја измами нивната IP адреса како што е видлива за owncloud.online. Повеќе информации може да најдете во нашата документација .", + "The reverse proxy headers configuration is incorrect, or you are accessing the server from a trusted proxy. If you are not accessing the server from a trusted proxy, this is a security issue and can allow an attacker to spoof their IP address as visible to the server. Further information can be found in our documentation." : "Конфигурација на заглавјето за прокси е неточна, или пристапувате до owncloud.online преку доверлив прокси. Ако не пристапувате до owncloud.online од доверлив прокси, ова е безбедносно прашање и може да дозволите напаѓачот да ја измами нивната IP адреса како што е видлива за owncloud.online. Повеќе информации може да најдете во нашата документација .", "Memcached is configured as distributed cache, but the wrong PHP module \"memcache\" is installed. \\OC\\Memcache\\Memcached only supports \"memcached\" and not \"memcache\". See the memcached wiki about both modules." : "Memcached е конфигуриран како дистрибуирана кеш меморија, но се инсталира погрешен PHP модул \"memcache\". \\OC\\Memcache\\Memcached поддржува само \"memcached\", а не \"memcache\". Погледнете ги memcached вики за двата модули .", "Some files have not passed the integrity check. Further information on how to resolve this issue can be found in our documentation. (List of invalid files… / Rescan…)" : "Некои датотеки не ја поминале проверката за интегритет. Повеќе информации за тоа како да се реши ова прашање може да се најдат во нашата документација. (Листа на невалидни датотеки… / Скенирај повторно…) ", "You are running in DEBUG mode, this can cause unwanted side effects. Please remove the \"debug\" flag from \"config.php\"." : "Работите во DEBUG режим, ова може да предизвика несакани ефекти. Ве молиме отстранете го знакот за \"debug\" од \"config.php\".", diff --git a/core/l10n/mk.json b/core/l10n/mk.json index 00fc4137e8..ac26377d4d 100644 --- a/core/l10n/mk.json +++ b/core/l10n/mk.json @@ -140,7 +140,7 @@ "No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our documentation." : "Не е конфигуриран кеш меморија. За да ги подобрите вашите перформанси, конфигурирајте memcache ако е достапно. Повеќе информации може да најдете во нашата документација .", "/dev/urandom is not readable by PHP which is highly discouraged for security reasons. Further information can be found in our documentation." : "/dev/urandom не може да се чита од PHP, што е многу обесхрабрен поради безбедносни причини. Повеќе информации може да најдете во нашата документација .", "You are currently running PHP {version}. We encourage you to upgrade your PHP version to take advantage of performance and security updates provided by the PHP Group as soon as your distribution supports it." : "Во моментов работиш со PHP {верзија}. Ве охрабруваме да ја надградите Вашата верзија на PHP за да ги искористите ажурирањата за ефикасноста и сигурноста обезбедени од страна на PHP Групата веднаш штом вашата дистрибуција го поддржува.", - "The reverse proxy headers configuration is incorrect, or you are accessing the server from a trusted proxy. If you are not accessing the server from a trusted proxy, this is a security issue and can allow an attacker to spoof their IP address as visible to the server. Further information can be found in our documentation." : "Конфигурација на заглавјето за прокси е неточна, или пристапувате до OwnCloud преку доверлив прокси. Ако не пристапувате до owncloud.online од доверлив прокси, ова е безбедносно прашање и може да дозволите напаѓачот да ја измами нивната IP адреса како што е видлива за owncloud.online. Повеќе информации може да најдете во нашата документација .", + "The reverse proxy headers configuration is incorrect, or you are accessing the server from a trusted proxy. If you are not accessing the server from a trusted proxy, this is a security issue and can allow an attacker to spoof their IP address as visible to the server. Further information can be found in our documentation." : "Конфигурација на заглавјето за прокси е неточна, или пристапувате до owncloud.online преку доверлив прокси. Ако не пристапувате до owncloud.online од доверлив прокси, ова е безбедносно прашање и може да дозволите напаѓачот да ја измами нивната IP адреса како што е видлива за owncloud.online. Повеќе информации може да најдете во нашата документација .", "Memcached is configured as distributed cache, but the wrong PHP module \"memcache\" is installed. \\OC\\Memcache\\Memcached only supports \"memcached\" and not \"memcache\". See the memcached wiki about both modules." : "Memcached е конфигуриран како дистрибуирана кеш меморија, но се инсталира погрешен PHP модул \"memcache\". \\OC\\Memcache\\Memcached поддржува само \"memcached\", а не \"memcache\". Погледнете ги memcached вики за двата модули .", "Some files have not passed the integrity check. Further information on how to resolve this issue can be found in our documentation. (List of invalid files… / Rescan…)" : "Некои датотеки не ја поминале проверката за интегритет. Повеќе информации за тоа како да се реши ова прашање може да се најдат во нашата документација. (Листа на невалидни датотеки… / Скенирај повторно…) ", "You are running in DEBUG mode, this can cause unwanted side effects. Please remove the \"debug\" flag from \"config.php\"." : "Работите во DEBUG режим, ова може да предизвика несакани ефекти. Ве молиме отстранете го знакот за \"debug\" од \"config.php\".", diff --git a/core/l10n/sl.js b/core/l10n/sl.js index d28bc79cc3..aa98d50902 100644 --- a/core/l10n/sl.js +++ b/core/l10n/sl.js @@ -132,7 +132,7 @@ OC.L10N.register( "Strong password" : "Odlično geslo", "Your web server is not yet set up properly to allow file synchronization because the WebDAV interface seems to be broken." : "Spletni stražnik še ni ustrezno nastavljen in ne omogoča usklajevanja, saj je vmesnik WebDAV videti okvarjen.", "Your web server is not set up properly to resolve \"{url}\". Further information can be found in our documentation." : "Spletni strežnik ni ustrezno nastavljen za razreševanje \"{url}\". Več pdorobnosti je zapisanih v dokumentaciji.", - "This server has no working Internet connection. This means that some of the features like mounting external storage, notifications about updates or installation of third-party apps will not work. Accessing files remotely and sending of notification emails might not work, either. We suggest to enable Internet connection for this server if you want to have all features." : "Strežnik je brez vzpostavljene internetne povezave. To pomeni, da priklapljanje zunanjih diskov, opozorila za posodobitve in namestitve programske opreme iz drugih virov, niso mogoče. Za popolno delovanje OwnCloud je nujna delujoča internetna povezava.", + "This server has no working Internet connection. This means that some of the features like mounting external storage, notifications about updates or installation of third-party apps will not work. Accessing files remotely and sending of notification emails might not work, either. We suggest to enable Internet connection for this server if you want to have all features." : "Strežnik je brez vzpostavljene internetne povezave. To pomeni, da priklapljanje zunanjih diskov, opozorila za posodobitve in namestitve programske opreme iz drugih virov, niso mogoče. Za popolno delovanje owncloud.online je nujna delujoča internetna povezava.", "No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our documentation." : "Ni nastavljenega predpomnilnika. Za izboljšanje hitrosti delovanja je treba predpomnilnik memcache, če je na voljo, ustrezno nastaviti. Več podrobnosti je na voljo v dokumentaciji.", "/dev/urandom is not readable by PHP which is highly discouraged for security reasons. Further information can be found in our documentation." : "Iz varnostnih razlogov je priporočljivo nastaviti dovoljenja ukaza /dev/urandom za branje prek PHP. Več podrobnosti je zavedenih v dokumentaciji<-a>", "You are currently running PHP {version}. We encourage you to upgrade your PHP version to take advantage of performance and security updates provided by the PHP Group as soon as your distribution supports it." : "Trenutno je zagnana različica PHP {version}. Priporočljivo je posodobiti sistem na najnovejšo različico in s tem namestiti funkcijske in varnostne posodobitve delovanja, ki jih zagotavlja skupnost PHP. Pakete je priporočljivo posodobiti takoj, ko so na voljo za nameščeno distribucijo.", diff --git a/core/l10n/sl.json b/core/l10n/sl.json index c95d51f28a..125b9956fa 100644 --- a/core/l10n/sl.json +++ b/core/l10n/sl.json @@ -130,7 +130,7 @@ "Strong password" : "Odlično geslo", "Your web server is not yet set up properly to allow file synchronization because the WebDAV interface seems to be broken." : "Spletni stražnik še ni ustrezno nastavljen in ne omogoča usklajevanja, saj je vmesnik WebDAV videti okvarjen.", "Your web server is not set up properly to resolve \"{url}\". Further information can be found in our documentation." : "Spletni strežnik ni ustrezno nastavljen za razreševanje \"{url}\". Več pdorobnosti je zapisanih v dokumentaciji.", - "This server has no working Internet connection. This means that some of the features like mounting external storage, notifications about updates or installation of third-party apps will not work. Accessing files remotely and sending of notification emails might not work, either. We suggest to enable Internet connection for this server if you want to have all features." : "Strežnik je brez vzpostavljene internetne povezave. To pomeni, da priklapljanje zunanjih diskov, opozorila za posodobitve in namestitve programske opreme iz drugih virov, niso mogoče. Za popolno delovanje OwnCloud je nujna delujoča internetna povezava.", + "This server has no working Internet connection. This means that some of the features like mounting external storage, notifications about updates or installation of third-party apps will not work. Accessing files remotely and sending of notification emails might not work, either. We suggest to enable Internet connection for this server if you want to have all features." : "Strežnik je brez vzpostavljene internetne povezave. To pomeni, da priklapljanje zunanjih diskov, opozorila za posodobitve in namestitve programske opreme iz drugih virov, niso mogoče. Za popolno delovanje owncloud.online je nujna delujoča internetna povezava.", "No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our documentation." : "Ni nastavljenega predpomnilnika. Za izboljšanje hitrosti delovanja je treba predpomnilnik memcache, če je na voljo, ustrezno nastaviti. Več podrobnosti je na voljo v dokumentaciji.", "/dev/urandom is not readable by PHP which is highly discouraged for security reasons. Further information can be found in our documentation." : "Iz varnostnih razlogov je priporočljivo nastaviti dovoljenja ukaza /dev/urandom za branje prek PHP. Več podrobnosti je zavedenih v dokumentaciji<-a>", "You are currently running PHP {version}. We encourage you to upgrade your PHP version to take advantage of performance and security updates provided by the PHP Group as soon as your distribution supports it." : "Trenutno je zagnana različica PHP {version}. Priporočljivo je posodobiti sistem na najnovejšo različico in s tem namestiti funkcijske in varnostne posodobitve delovanja, ki jih zagotavlja skupnost PHP. Pakete je priporočljivo posodobiti takoj, ko so na voljo za nameščeno distribucijo.", diff --git a/core/l10n/sr.js b/core/l10n/sr.js index 55fdb9cd86..8d09ab9e86 100644 --- a/core/l10n/sr.js +++ b/core/l10n/sr.js @@ -160,7 +160,7 @@ OC.L10N.register( "Please reload the page." : "Поново учитајте страницу.", "The update was unsuccessful. Please check the server log or contact your administrator." : "Ажурирање није успело. Проверите дневник сервера или контактирајте вашег администратора.", "The update was successful. There were warnings." : "Ажурирање је успело. Било је упозорења.", - "The update was successful. Redirecting you to owncloud.online now." : "Ажурирање је успело. Преусмеравам вас на оунКлауд.", + "The update was successful. Redirecting you to owncloud.online now." : "Ажурирање је успело. Преусмеравам вас на owncloud.online.", "Searching other places" : "Претражујем остала места", "Personal" : "Лично", "Users" : "Корисници", @@ -219,7 +219,7 @@ OC.L10N.register( "Use the following link to reset your password: {link}" : "Употребите следећу везу да ресетујете своју лозинку: {link}", "New password" : "Нова лозинка", "Reset password" : "Ресетуј лозинку", - "This owncloud.online instance is currently in single user mode." : "Овај оунКлауд тренутно ради у режиму једног корисника.", + "This owncloud.online instance is currently in single user mode." : "Овај owncloud.online тренутно ради у режиму једног корисника.", "This means only administrators can use the instance." : "То значи да га могу користити само администратори.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Контактирајте администратора ако се порука понавља или се неочекивано појавила.", "Thank you for your patience." : "Хвала вам на стрпљењу.", diff --git a/core/l10n/sr.json b/core/l10n/sr.json index 839c596bfa..d1b35a5df0 100644 --- a/core/l10n/sr.json +++ b/core/l10n/sr.json @@ -158,7 +158,7 @@ "Please reload the page." : "Поново учитајте страницу.", "The update was unsuccessful. Please check the server log or contact your administrator." : "Ажурирање није успело. Проверите дневник сервера или контактирајте вашег администратора.", "The update was successful. There were warnings." : "Ажурирање је успело. Било је упозорења.", - "The update was successful. Redirecting you to owncloud.online now." : "Ажурирање је успело. Преусмеравам вас на оунКлауд.", + "The update was successful. Redirecting you to owncloud.online now." : "Ажурирање је успело. Преусмеравам вас на owncloud.online.", "Searching other places" : "Претражујем остала места", "Personal" : "Лично", "Users" : "Корисници", @@ -217,7 +217,7 @@ "Use the following link to reset your password: {link}" : "Употребите следећу везу да ресетујете своју лозинку: {link}", "New password" : "Нова лозинка", "Reset password" : "Ресетуј лозинку", - "This owncloud.online instance is currently in single user mode." : "Овај оунКлауд тренутно ради у режиму једног корисника.", + "This owncloud.online instance is currently in single user mode." : "Овај owncloud.online тренутно ради у режиму једног корисника.", "This means only administrators can use the instance." : "То значи да га могу користити само администратори.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Контактирајте администратора ако се порука понавља или се неочекивано појавила.", "Thank you for your patience." : "Хвала вам на стрпљењу.", diff --git a/core/l10n/sv.js b/core/l10n/sv.js index 25a751a4b8..f9ca582df9 100644 --- a/core/l10n/sv.js +++ b/core/l10n/sv.js @@ -154,7 +154,7 @@ OC.L10N.register( "No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our documentation." : "Ingen minnes cache har blivit konfigurerad. För att förbättra din prestanda var god konfigurera memcache om tillgängligt. Vidare information kan finnas i vår dokumentation.", "/dev/urandom is not readable by PHP which is highly discouraged for security reasons. Further information can be found in our documentation." : "/dev/urandom är inte läsbar av PHP vilket definitivt inte är rekommenderat av säkerhetsskäl. Vidare information kan finnas i vår dokumentation.", "You are currently running PHP {version}. We encourage you to upgrade your PHP version to take advantage of performance and security updates provided by the PHP Group as soon as your distribution supports it." : "Du kör för närvarande PHP {version}. Vi rekommenderar dig att uppgradera din PHP version så att ta fördel utav prestanda och säkerhetsuppdateringar från PHP Group så fort som din distribution stödjer det.", - "The reverse proxy headers configuration is incorrect, or you are accessing the server from a trusted proxy. If you are not accessing the server from a trusted proxy, this is a security issue and can allow an attacker to spoof their IP address as visible to the server. Further information can be found in our documentation." : "Konfiguration för \"reverse proxy headers\" är felaktig eller så försöker du nå Owncloud från en betrodd proxy. Om du inte försöker nå Owncloud från en betrodd proxy, detta är en säkerhetsrisk och kan möjliggöra att en hacker att förfalska sin IP adress som är synlig för Owncloud. Vidare information kan finnas i vår dokumentation.", + "The reverse proxy headers configuration is incorrect, or you are accessing the server from a trusted proxy. If you are not accessing the server from a trusted proxy, this is a security issue and can allow an attacker to spoof their IP address as visible to the server. Further information can be found in our documentation." : "Konfiguration för \"reverse proxy headers\" är felaktig eller så försöker du nå owncloud.online från en betrodd proxy. Om du inte försöker nå owncloud.online från en betrodd proxy, detta är en säkerhetsrisk och kan möjliggöra att en hacker att förfalska sin IP adress som är synlig för owncloud.online. Vidare information kan finnas i vår dokumentation.", "Memcached is configured as distributed cache, but the wrong PHP module \"memcache\" is installed. \\OC\\Memcache\\Memcached only supports \"memcached\" and not \"memcache\". See the memcached wiki about both modules." : "Memcached är konfigurerad som distribuerad cache, men fel PHP modul \"memcache\" är installerad. \\OC\\Memcache\\Memcached stödjer bara \"memcached\" och inte \"memcache\". Se wiki för memcached för båda modulerna.", "Some files have not passed the integrity check. Further information on how to resolve this issue can be found in our documentation. (List of invalid files… / Rescan…)" : "Några filer passerade inte integritetskontrollen. Vidare information om hur man löser dessa problem kan finnas i vår dokumentation. (Lista över felaktiga filer… / Sök igenom igen…)", "You are running in DEBUG mode, this can cause unwanted side effects. Please remove the \"debug\" flag from \"config.php\"." : "Du kör i DEBUG-läge, detta kan orsaka oönskade bieffekter. Ta bort \"debug\"-flaggan från \"config.php\".", diff --git a/core/l10n/sv.json b/core/l10n/sv.json index 4be5a5e61d..8b35110c26 100644 --- a/core/l10n/sv.json +++ b/core/l10n/sv.json @@ -152,7 +152,7 @@ "No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our documentation." : "Ingen minnes cache har blivit konfigurerad. För att förbättra din prestanda var god konfigurera memcache om tillgängligt. Vidare information kan finnas i vår dokumentation.", "/dev/urandom is not readable by PHP which is highly discouraged for security reasons. Further information can be found in our documentation." : "/dev/urandom är inte läsbar av PHP vilket definitivt inte är rekommenderat av säkerhetsskäl. Vidare information kan finnas i vår dokumentation.", "You are currently running PHP {version}. We encourage you to upgrade your PHP version to take advantage of performance and security updates provided by the PHP Group as soon as your distribution supports it." : "Du kör för närvarande PHP {version}. Vi rekommenderar dig att uppgradera din PHP version så att ta fördel utav prestanda och säkerhetsuppdateringar från PHP Group så fort som din distribution stödjer det.", - "The reverse proxy headers configuration is incorrect, or you are accessing the server from a trusted proxy. If you are not accessing the server from a trusted proxy, this is a security issue and can allow an attacker to spoof their IP address as visible to the server. Further information can be found in our documentation." : "Konfiguration för \"reverse proxy headers\" är felaktig eller så försöker du nå Owncloud från en betrodd proxy. Om du inte försöker nå Owncloud från en betrodd proxy, detta är en säkerhetsrisk och kan möjliggöra att en hacker att förfalska sin IP adress som är synlig för Owncloud. Vidare information kan finnas i vår dokumentation.", + "The reverse proxy headers configuration is incorrect, or you are accessing the server from a trusted proxy. If you are not accessing the server from a trusted proxy, this is a security issue and can allow an attacker to spoof their IP address as visible to the server. Further information can be found in our documentation." : "Konfiguration för \"reverse proxy headers\" är felaktig eller så försöker du nå owncloud.online från en betrodd proxy. Om du inte försöker nå owncloud.online från en betrodd proxy, detta är en säkerhetsrisk och kan möjliggöra att en hacker att förfalska sin IP adress som är synlig för owncloud.online. Vidare information kan finnas i vår dokumentation.", "Memcached is configured as distributed cache, but the wrong PHP module \"memcache\" is installed. \\OC\\Memcache\\Memcached only supports \"memcached\" and not \"memcache\". See the memcached wiki about both modules." : "Memcached är konfigurerad som distribuerad cache, men fel PHP modul \"memcache\" är installerad. \\OC\\Memcache\\Memcached stödjer bara \"memcached\" och inte \"memcache\". Se wiki för memcached för båda modulerna.", "Some files have not passed the integrity check. Further information on how to resolve this issue can be found in our documentation. (List of invalid files… / Rescan…)" : "Några filer passerade inte integritetskontrollen. Vidare information om hur man löser dessa problem kan finnas i vår dokumentation. (Lista över felaktiga filer… / Sök igenom igen…)", "You are running in DEBUG mode, this can cause unwanted side effects. Please remove the \"debug\" flag from \"config.php\"." : "Du kör i DEBUG-läge, detta kan orsaka oönskade bieffekter. Ta bort \"debug\"-flaggan från \"config.php\".", diff --git a/tests/lib/L10N/ProductNameTranslationTest.php b/tests/lib/L10N/ProductNameTranslationTest.php new file mode 100644 index 0000000000..28054cbbb9 --- /dev/null +++ b/tests/lib/L10N/ProductNameTranslationTest.php @@ -0,0 +1,121 @@ +. + */ + +namespace Test\L10N; + +use Test\TestCase; + +/** + * Sichtbare Übersetzungen nennen das Produkt owncloud.online. Geprüft werden + * die Übersetzungsdateien des Kerns und der mitgelieferten Apps, jeweils die + * .json (PHP) und die .js (Browser), auf Schreibvarianten des alten Namens. + * + * Nicht gemeint sind Pfade wie username@example.com/owncloud in der + * Beispielsyntax beim Teilen: Davor steht ein Schrägstrich. + */ +class ProductNameTranslationTest extends TestCase { + /** + * Alter Name samt Tippfehlern und Umschrift: ownCloud, OwnCloud, Owncloud, + * ownClound, ownClud, ownClowd, onwCloud, wnCloud, #owncloud, оунКлауд. + * Davor darf weder ein Buchstabe noch ein Schrägstrich stehen, dahinter + * nicht „.online“. + */ + private const OLD_PRODUCT_NAME = '/(?assertNotEmpty($files); + + $found = []; + foreach ($files as $file) { + $translations = self::readTranslations($file); + $this->assertIsArray($translations, "$file lässt sich nicht lesen"); + foreach ($translations as $key => $value) { + foreach ((array)$value as $text) { + if (\preg_match_all(self::OLD_PRODUCT_NAME, $text, $matches)) { + $found[] = \substr($file, \strlen(\OC::$SERVERROOT) + 1) + . ": $key => " . \implode(', ', $matches[0]); + } + } + } + } + + $this->assertSame([], $found, "Alter Produktname in Übersetzungen:\n" . \implode("\n", $found)); + } + + public function providesOldAndCurrentNames() { + return [ + 'alter Name' => ['Diese ownCloud-Instanz', true], + 'Tippfehler' => ['Diese ownClound-Instanz', true], + 'Großschreibung' => ['Servidor OwnCloud', true], + 'Hashtag' => ['Federated Cloud #owncloud %s', true], + 'kyrillische Umschrift' => ['Поверљиви оунКлауд сервери', true], + 'ohne o' => ['núcleo wnCloud', true], + 'neuer Name' => ['Diese owncloud.online-Instanz', false], + 'neuer Name als Hashtag' => ['Federated Cloud #owncloud.online %s', false], + 'Pfad im Beispiel' => ['username@example.com/owncloud', false], + 'Pfad mit Tippfehler im Beispiel' => ['nomeusuario@exemplo.com/ouwncloud', false], + 'anderes Wort' => ['Download', false], + ]; + } + + /** + * @dataProvider providesOldAndCurrentNames + * + * @param string $text + * @param bool $isOldName + */ + public function testPatternRecognisesOnlyTheOldName($text, $isOldName) { + $this->assertSame($isOldName, \preg_match(self::OLD_PRODUCT_NAME, $text) === 1); + } +} From 467d4795b5d4d484324a39f082d4e4279b640d2c Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Sat, 17 Oct 2026 01:57:04 +0200 Subject: [PATCH 02/11] fix(login): fehlendes Anmeldefeld ohne Deprecation, Arrays sofort abweisen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LoginController::arrayAsMissing() macht aus user[]=x – wie aus einem fehlenden Feld – null. tryLogin() reicht das über Session::login() an Manager::checkPassword() weiter, und dort stand str_replace("\0", '', null): seit PHP 8.1 „Passing null to parameter #3 ($subject)“, einmal für den Namen, einmal für das Passwort. checkPassword() behandelt null jetzt als leeres Feld ((string)); für Zeichenketten ändert sich nichts, Bremse und Backends bekommen '' wie bei einem leer abgeschickten Feld. Ein Array als Name oder Passwort – das kann über andere Aufrufer wie checkPerson (OCS) oder den Token-Endpunkt noch ankommen – endet sofort mit false. Bisher lief es bis in die Brute-Force-Bremse („Array to string conversion“ im Doctrine-Treiber) und in die Backends. Test: ManagerTest::testCheckPasswordTreatsMissingFieldsAsEmpty fängt E_DEPRECATED selbst ab (phpunit-autotest.xml blendet Deprecations aus): vorher zwei Meldungen aus str_replace(), danach keine; das Backend bekommt ''. testCheckPasswordRejectsArrays: vorher erreichte das Array Database::checkPassword(), jetzt nicht mehr. --- .../login-missing-field-deprecation | 8 +++ lib/private/User/Manager.php | 14 ++++- tests/lib/User/ManagerTest.php | 57 +++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 changelog/unreleased/login-missing-field-deprecation diff --git a/changelog/unreleased/login-missing-field-deprecation b/changelog/unreleased/login-missing-field-deprecation new file mode 100644 index 0000000000..849c859e24 --- /dev/null +++ b/changelog/unreleased/login-missing-field-deprecation @@ -0,0 +1,8 @@ +Bugfix: A login without user name no longer logs a PHP deprecation + +A login form sent without the user or password field, or with one of them as a +list (user[]=x), reached the password check with null. PHP then logged +"Passing null to parameter" from str_replace() for each of them. A missing +field now counts as empty. A list as user name or password is refused before +the brute force throttle or a user backend sees it. The login still fails with +"Wrong password". diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index daad1e1af9..6429f64b0c 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -35,6 +35,7 @@ * Changes: * - throttle password spraying per-IP, reset delay on successful auth * - Bruteforce- und Rate-Limiting-Schutz hinzufügen + * - checkPassword(): fehlendes Feld (null) als leer, Arrays sofort abweisen */ namespace OC\User; @@ -258,8 +259,17 @@ public function userExists($uid) { * @return mixed the User object on success, false otherwise */ public function checkPassword($loginName, $password) { - $loginName = \str_replace("\0", '', $loginName); - $password = \str_replace("\0", '', $password); + // Ein Array (user[]=x, password[]=x) ist weder Name noch Passwort und + // darf weder die Bremse noch ein Backend erreichen. + if (\is_array($loginName) || \is_array($password)) { + return false; + } + // Ein fehlendes Feld kommt als null an, etwa vom Anmeldeformular ohne + // user-Feld oder mit user[]=x (LoginController::arrayAsMissing()). + // Gemeint ist ein leeres Feld; str_replace() mit null ist seit PHP 8.1 + // veraltet. + $loginName = \str_replace("\0", '', (string)$loginName); + $password = \str_replace("\0", '', (string)$password); $ip = \OC::$server->getRequest()->getRemoteAddress(); /** @var Throttler $throttler */ diff --git a/tests/lib/User/ManagerTest.php b/tests/lib/User/ManagerTest.php index f636b5dda8..c86aaff8d1 100644 --- a/tests/lib/User/ManagerTest.php +++ b/tests/lib/User/ManagerTest.php @@ -143,6 +143,63 @@ public function testCheckPasswordNotSupported() { $this->assertFalse($this->manager->checkPassword('foo', 'bar')); } + /** + * Ein fehlendes Anmeldefeld kommt als null an: Anmeldeformular ohne + * user-Feld oder mit user[]=x, das der LoginController wie ein fehlendes + * Feld behandelt. Gemeint ist ein leeres Feld – ohne „Passing null to + * parameter“ aus str_replace(), das Backend bekommt ''. + */ + public function testCheckPasswordTreatsMissingFieldsAsEmpty() { + /** @var Database | \PHPUnit\Framework\MockObject\MockObject $backend */ + $backend = $this->createMock(Database::class); + $backend->method('implementsActions')->willReturn(true); + $backend->expects($this->once()) + ->method('checkPassword') + ->with($this->identicalTo(''), $this->identicalTo('')) + ->willReturn(false); + $this->manager->registerBackend($backend); + + $deprecations = []; + \set_error_handler(function ($errno, $errstr) use (&$deprecations) { + $deprecations[] = $errstr; + return true; + }, E_DEPRECATED); + try { + $result = $this->manager->checkPassword(null, null); + } finally { + \restore_error_handler(); + } + + $this->assertFalse($result); + $this->assertSame([], $deprecations); + } + + public function providesArrayCredentials() { + return [ + 'Name als Array' => [['admin'], 'secret'], + 'Passwort als Array' => ['admin', ['secret']], + ]; + } + + /** + * Ein Array ist weder Anmeldename noch Passwort. Es erreicht weder die + * Bremse noch ein Backend. + * + * @dataProvider providesArrayCredentials + * + * @param mixed $loginName + * @param mixed $password + */ + public function testCheckPasswordRejectsArrays($loginName, $password) { + /** @var Database | \PHPUnit\Framework\MockObject\MockObject $backend */ + $backend = $this->createMock(Database::class); + $backend->method('implementsActions')->willReturn(true); + $backend->expects($this->never())->method('checkPassword'); + $this->manager->registerBackend($backend); + + $this->assertFalse($this->manager->checkPassword($loginName, $password)); + } + public function testGetAccountExists() { $account = new Account(); $account->setUserId('foo'); From 07216aeffd517892fb7376e59a3d2ab094a65d11 Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Sun, 18 Oct 2026 21:05:50 +0200 Subject: [PATCH 03/11] fix(login): Reset-Token erst unmittelbar vor dem Versand speichern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LostController::sendEmail() speicherte den Token vor dem Zusammenstellen der Mail. Der Fehlerzweig aus 4465e99dbd, der ihn bei gescheitertem Versand wieder entfernt, lag nur um createMessage() … send(). Scheiterte vorher etwas – Vorlagen (OC_Template::fetchPage()), Anzeigename des Empfängers –, blieb der Token stehen: Fünf Minuten lang galt jede neue Anforderung als „recently sent“, verschickte nichts und meldete trotzdem Erfolg. Jetzt schreibt sendEmail() den Token im try unmittelbar vor mailer->send(). Scheitert vorher etwas, bleibt der Stand von vor der Anforderung erhalten – ein älterer, noch gültiger Link wird dann auch nicht entwertet. Der catch entfernt nur einen Token, den diese Anforderung selbst gespeichert hat; scheitert send(), bleibt es beim Verhalten aus 4465e99dbd. Fehlermeldungen und die Fünf-Minuten-Sperre nach einer zugestellten Mail sind unverändert. Test: LostControllerTest::testFailureBeforeSendingLeavesNoRecentlySentToken lässt das Zusammenstellen beim ersten Versuch scheitern (Anzeigename wirft) und fordert eine Minute später erneut an. Vorher stand danach „12000:FirstToken“, der zweite Versuch verschickte nichts; jetzt ist kein Token gespeichert und der zweite Versuch verschickt die Mail. --- .../lostpassword-token-only-before-sending | 8 +++ core/Controller/LostController.php | 17 +++-- tests/Core/Controller/LostControllerTest.php | 62 +++++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 changelog/unreleased/lostpassword-token-only-before-sending diff --git a/changelog/unreleased/lostpassword-token-only-before-sending b/changelog/unreleased/lostpassword-token-only-before-sending new file mode 100644 index 0000000000..169d0c889e --- /dev/null +++ b/changelog/unreleased/lostpassword-token-only-before-sending @@ -0,0 +1,8 @@ +Bugfix: A reset mail that fails before sending no longer blocks the next request + +The password reset stored its token before the mail was put together. When +building the mail failed, for example while rendering its templates, the token +stayed stored and every new request within the next five minutes counted as a +repeated one: no mail was sent, but the page reported success. The token is +now stored right before the mail is handed to the mail server, so such a +failure leaves the account as it was before the request. diff --git a/core/Controller/LostController.php b/core/Controller/LostController.php index 8044007e96..3089372bb0 100644 --- a/core/Controller/LostController.php +++ b/core/Controller/LostController.php @@ -30,6 +30,8 @@ * Changes: * - PHP 8.4 compatibility and owncloud.online design integration * - php8.3 (#41449) + * - Reset-Token erst unmittelbar vor dem Versand speichern, ohne Mail + * keinen „recently sent“-Zustand hinterlassen */ namespace OC\Core\Controller; @@ -378,8 +380,6 @@ public function sendEmail($user, $token, $link) { } } - $this->config->setUserValue($user, 'owncloud', 'lostpassword', $this->timeFactory->getTime() . ':' . $token); - $tmpl = new \OC_Template('core', 'lostpassword/email'); $tmpl->assign('link', $link); $msg = $tmpl->fetchPage(); @@ -388,6 +388,7 @@ public function sendEmail($user, $token, $link) { $msgAlt = $tmplAlt->fetchPage(); $name = $userObject->getDisplayName(); + $tokenStored = false; try { $message = $this->mailer->createMessage(); $message->setTo([$email => $name]); @@ -395,12 +396,20 @@ public function sendEmail($user, $token, $link) { $message->setPlainBody($msgAlt); $message->setHtmlBody($msg); $message->setFrom([$this->from => $this->defaults->getName()]); + // Den Token erst unmittelbar vor dem Versand speichern: Scheitert + // vorher etwas (Vorlagen, Empfänger, Nachricht), bleibt der Stand + // vor der Anforderung erhalten und nichts gilt als „recently sent“. + $this->config->setUserValue($user, 'owncloud', 'lostpassword', $this->timeFactory->getTime() . ':' . $token); + $tokenStored = true; $this->mailer->send($message); } catch (\Exception $e) { // Ohne zugestellte Mail darf der Token nicht stehen bleiben: Er galt // sonst fünf Minuten lang als „recently sent“, „Erneut anfordern“ - // verschickte nichts und email() meldete trotzdem Erfolg. - $this->config->deleteUserValue($user, 'owncloud', 'lostpassword'); + // verschickte nichts und email() meldete trotzdem Erfolg. Entfernt + // wird nur ein Token, den diese Anforderung selbst gespeichert hat. + if ($tokenStored) { + $this->config->deleteUserValue($user, 'owncloud', 'lostpassword'); + } throw new \Exception($this->l10n->t( 'Couldn\'t send reset email. Please contact your administrator.' )); diff --git a/tests/Core/Controller/LostControllerTest.php b/tests/Core/Controller/LostControllerTest.php index 36e43f49cd..34f5a0b69e 100644 --- a/tests/Core/Controller/LostControllerTest.php +++ b/tests/Core/Controller/LostControllerTest.php @@ -666,6 +666,68 @@ public function testRetryWithinFiveMinutesAfterFailedSendSendsTheMail() { $this->assertSame('12060:SecondToken', $userValues['ExistingUser/owncloud/lostpassword'] ?? null); } + /** + * Scheitert das Zusammenstellen der Mail schon vor dem Versand (Vorlagen, + * Empfängername), darf kein Token gespeichert bleiben. Sonst galt der + * nächste Versuch fünf Minuten lang als „recently sent“: Er verschickte + * nichts und meldete trotzdem Erfolg. + */ + public function testFailureBeforeSendingLeavesNoRecentlySentToken() { + $userValues = []; + $this->config->method('getUserValue') + ->willReturnCallback(function ($userId, $appName, $key, $default = '') use (&$userValues) { + return $userValues["$userId/$appName/$key"] ?? $default; + }); + $this->config->method('setUserValue') + ->willReturnCallback(function ($userId, $appName, $key, $value) use (&$userValues) { + $userValues["$userId/$appName/$key"] = $value; + }); + $this->config->method('deleteUserValue') + ->willReturnCallback(function ($userId, $appName, $key) use (&$userValues) { + unset($userValues["$userId/$appName/$key"]); + }); + // Beim ersten Versuch scheitert das Zusammenstellen der Mail + $user = $this->createMock(IUser::class); + $user->method('getEMailAddress')->willReturn('test@example.com'); + $user->method('getUID')->willReturn('ExistingUser'); + $nameLookups = 0; + $user->method('getDisplayName')->willReturnCallback(function () use (&$nameLookups) { + $nameLookups++; + if ($nameLookups === 1) { + throw new \Exception('Anzeigename nicht lesbar'); + } + return 'Existing User Name'; + }); + $this->userManager->method('userExists')->with('ExistingUser')->willReturn(true); + $this->userManager->method('get')->with('ExistingUser')->willReturn($user); + $this->secureRandom->method('generate')->willReturnOnConsecutiveCalls('FirstToken', 'SecondToken'); + $this->urlGenerator->method('linkToRouteAbsolute')->willReturn('https://example.com/index.php/lostpassword/'); + $now = 12000; + $this->timeFactory->method('getTime')->willReturnCallback(function () use (&$now) { + return $now; + }); + $message = $this->getMockBuilder('\OC\Mail\Message') + ->disableOriginalConstructor()->getMock(); + $this->mailer->method('createMessage')->willReturn($message); + $sendAttempts = 0; + $this->mailer->method('send')->willReturnCallback(function () use (&$sendAttempts) { + $sendAttempts++; + return []; + }); + + $firstResponse = $this->lostController->email('ExistingUser'); + $tokenAfterFailure = $userValues['ExistingUser/owncloud/lostpassword'] ?? null; + // „Erneut anfordern“ eine Minute später + $now = 12060; + $retryResponse = $this->lostController->email('ExistingUser'); + + $this->assertSame('error', $firstResponse['status']); + $this->assertNull($tokenAfterFailure, 'Ohne Versand darf kein Token gespeichert sein.'); + $this->assertSame(1, $sendAttempts, 'Der zweite Versuch muss die Mail wirklich verschicken.'); + $this->assertSame(['status' => 'success'], $retryResponse); + $this->assertSame('12060:SecondToken', $userValues['ExistingUser/owncloud/lostpassword'] ?? null); + } + public function testSetPasswordUnsuccessful() { $this->config ->expects($this->once()) From ed6b897ae3aeab784717d5243502a8e888fc884b Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Sun, 18 Oct 2026 21:10:22 +0200 Subject: [PATCH 04/11] fix(setup): Markt-App bei der Neuinstallation aktivieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setup::install() rief Installer::installShippedApps() auf, bevor apps_paths mit apps-external in config.php stand. \OC::$APPSROOTS entsteht beim Start des Prozesses aus apps_paths, bei einer Neuinstallation also nur aus apps/. installShippedApps() fand die Markt-App (apps-external/market, default_enable) deshalb nicht: Sie blieb nach occ maintenance:install und nach dem Web-Installer aus, bis occ upgrade sie beim ersten Update nachholte. docs/installation/linux-server.md §10 („wird bei der Installation aktiviert“) stimmte damit nicht. Entscheidung: Reihenfolge korrigieren statt die Doku zurückzunehmen. Der Eintrag ist derselbe wie bisher und wie bisher nur, wenn apps_paths fehlt; er steht jetzt vor installShippedApps() (Setup::addDefaultAppsPaths()). Vorhandene Verzeichnisse, die der laufende Prozess noch nicht kennt, kommen zusätzlich in \OC::$APPSROOTS. Die Markt-App bringt weder Tabellen noch install.php mit; installiert wird genau das, was occ upgrade bisher beim ersten Update nachholte, nur früher. Bestehende Installationen, Updates und vorgegebene apps_paths ändern sich nicht. In der CI ist die Markt-App nach der Installation damit ebenfalls an; tests/apps.php lädt nur Tests aus apps/. Tests: SetupTest::testAddDefaultAppsPaths* (eigenes Serververzeichnis, drei Fälle). Nach dem Herauslösen der Methode ohne Verhaltensänderung rot (\OC::$APPSROOTS ohne apps-external), danach grün. Probe mit frischer occ maintenance:install (sqlite) in einer eigenen WSL-Kopie: vorher „market enabled“ leer, danach „yes“, installed_version 0.10.10, CheckUpdateBackgroundJob eingetragen, Log ohne Fehler. App-, Installer- und Repair\Apps-Tests laufen mit aktiver Markt-App grün. --- .../unreleased/market-enabled-on-install | 8 ++ lib/private/Setup.php | 61 ++++++++++------ tests/lib/SetupTest.php | 73 +++++++++++++++++++ 3 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 changelog/unreleased/market-enabled-on-install diff --git a/changelog/unreleased/market-enabled-on-install b/changelog/unreleased/market-enabled-on-install new file mode 100644 index 0000000000..e1870cf328 --- /dev/null +++ b/changelog/unreleased/market-enabled-on-install @@ -0,0 +1,8 @@ +Bugfix: The market app is enabled right after the installation + +A new installation registered apps-external as an app directory only after the +bundled apps had been installed. The installer therefore did not see the market +app there, and it stayed disabled until the first update ran occ upgrade. The +app directories are now registered first and are known to the running +installation, so occ maintenance:install and the web installer enable the +market app, as the installation guide describes. diff --git a/lib/private/Setup.php b/lib/private/Setup.php index 271ae65dbf..2683f09d3b 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -42,6 +42,8 @@ * - htaccess RewriteBase rules block API requests for files with common exten... * - Improve large filecache performance * - PHP 8.4 compatibility and owncloud.online design integration + * - apps_paths vor installShippedApps() eintragen, damit die Apps aus + * apps-external (Markt-App) bei der Installation aktiviert werden */ namespace OC; @@ -395,6 +397,10 @@ public function install($options) { $group =\OC::$server->getGroupManager()->createGroup('admin'); $group->addUser($user); + // apps-external zuerst eintragen, sonst übersieht die Installation + // die Apps dort, allen voran die Markt-App. + self::addDefaultAppsPaths($config); + //guess what this does Installer::installShippedApps(); @@ -416,29 +422,6 @@ public function install($options) { $config->setSystemValue('logtimezone', \date_default_timezone_get()); } - // add the apps-external directory to config using apps_path - // add the key only if it does not exist (protect against overwriting) - $apps2Key = \OC::$server->getSystemConfig()->getValue('apps_paths', false); - - if ($apps2Key === false) { - $defaultAppsPaths = [ - 'apps_paths' => [ - [ - "path" => \OC::$SERVERROOT . '/apps', - "url" => "/apps", - "writable" => false - ], - [ - "path" => \OC::$SERVERROOT . '/apps-external', - "url" => "/apps-external", - "writable" => true - ] - ] - ]; - - $config->setSystemValues($defaultAppsPaths); - } - self::installBackgroundJobs(); // save the origin version that we installed at @@ -453,6 +436,38 @@ public function install($options) { return $error; } + /** + * Trägt apps und apps-external als App-Pfade in config.php ein, sofern dort + * noch keine apps_paths stehen, und macht sie dem laufenden Prozess sofort + * bekannt. + * + * \OC::$APPSROOTS entsteht beim Start aus apps_paths. Bei der + * Neuinstallation fehlt der Schlüssel dann noch, der Prozess kennt nur + * apps/. Ohne den Nachtrag fand installShippedApps() die Apps in + * apps-external nicht: Die Markt-App blieb nach der Installation aus, bis + * occ upgrade sie beim nächsten Update nachholte. + */ + public static function addDefaultAppsPaths(IConfig $config): void { + // add the key only if it does not exist (protect against overwriting) + if ($config->getSystemValue('apps_paths', false) !== false) { + return; + } + $appsPaths = [ + ['path' => \OC::$SERVERROOT . '/apps', 'url' => '/apps', 'writable' => false], + ['path' => \OC::$SERVERROOT . '/apps-external', 'url' => '/apps-external', 'writable' => true], + ]; + $config->setSystemValues(['apps_paths' => $appsPaths]); + + // Nur vorhandene Verzeichnisse, die der Prozess noch nicht kennt: + // getShippedApps() durchsucht jedes davon. + $known = \array_column(\OC::$APPSROOTS, 'path'); + foreach ($appsPaths as $appsPath) { + if (!\in_array($appsPath['path'], $known, true) && \is_dir($appsPath['path'])) { + \OC::$APPSROOTS[] = $appsPath; + } + } + } + public static function installBackgroundJobs() { \OC::$server->getJobList()->add('\OC\Authentication\Token\DefaultTokenCleanupJob'); } diff --git a/tests/lib/SetupTest.php b/tests/lib/SetupTest.php index 4b40024b07..b27e2026c4 100644 --- a/tests/lib/SetupTest.php +++ b/tests/lib/SetupTest.php @@ -282,4 +282,77 @@ public function testUpdateHtaccessWithRewriteBaseUsesFileExistenceCheck(): void $content ); } + + /** + * Führt $test mit einem eigenen Serververzeichnis aus, das nur die + * genannten Unterordner hat. \OC::$APPSROOTS steht dabei so, wie + * \OC::initPaths() es ohne apps_paths in config.php setzt: nur apps/. + * + * @param string[] $dirs + * @param callable $test bekommt das Serververzeichnis + */ + private function withServerRoot(array $dirs, callable $test) { + $root = \rtrim(\OC::$server->getTempManager()->getTemporaryFolder(), '/'); + foreach ($dirs as $dir) { + \mkdir("$root/$dir"); + } + $origServerRoot = \OC::$SERVERROOT; + $origAppsRoots = \OC::$APPSROOTS; + \OC::$SERVERROOT = $root; + \OC::$APPSROOTS = [['path' => "$root/apps", 'url' => '/apps', 'writable' => true]]; + try { + $test($root); + } finally { + \OC::$SERVERROOT = $origServerRoot; + \OC::$APPSROOTS = $origAppsRoots; + } + } + + /** + * Bei der Neuinstallation fehlen apps_paths noch. Der laufende Prozess muss + * apps-external sofort kennen, sonst findet installShippedApps() dort die + * Markt-App nicht und sie bleibt bis zum ersten occ upgrade aus. + */ + public function testAddDefaultAppsPathsMakesAppsExternalKnownToTheRunningProcess() { + $this->withServerRoot(['apps', 'apps-external'], function ($root) { + $this->config->method('getSystemValue')->with('apps_paths', false)->willReturn(false); + $this->config->expects($this->once()) + ->method('setSystemValues') + ->with(['apps_paths' => [ + ['path' => "$root/apps", 'url' => '/apps', 'writable' => false], + ['path' => "$root/apps-external", 'url' => '/apps-external', 'writable' => true], + ]]); + + \OC\Setup::addDefaultAppsPaths($this->config); + + $this->assertSame(["$root/apps", "$root/apps-external"], \array_column(\OC::$APPSROOTS, 'path')); + }); + } + + public function testAddDefaultAppsPathsKeepsConfiguredAppsPaths() { + $this->withServerRoot(['apps', 'apps-external'], function ($root) { + $configured = [['path' => "$root/apps", 'url' => '/apps', 'writable' => true]]; + $this->config->method('getSystemValue')->with('apps_paths', false)->willReturn($configured); + $this->config->expects($this->never())->method('setSystemValues'); + + \OC\Setup::addDefaultAppsPaths($this->config); + + $this->assertSame(["$root/apps"], \array_column(\OC::$APPSROOTS, 'path')); + }); + } + + /** + * Ohne apps-external auf der Platte bleibt der Prozess bei apps/, ein + * fehlendes Verzeichnis gäbe dort nur Warnungen beim Durchsuchen. + */ + public function testAddDefaultAppsPathsSkipsMissingDirectoriesForTheRunningProcess() { + $this->withServerRoot(['apps'], function ($root) { + $this->config->method('getSystemValue')->with('apps_paths', false)->willReturn(false); + $this->config->expects($this->once())->method('setSystemValues'); + + \OC\Setup::addDefaultAppsPaths($this->config); + + $this->assertSame(["$root/apps"], \array_column(\OC::$APPSROOTS, 'path')); + }); + } } From b8a070aa2bedaea58647a58ea1a488d894465d57 Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Tue, 20 Oct 2026 21:04:36 +0200 Subject: [PATCH 05/11] =?UTF-8?q?fix(session):=20Widerruf=20beendet=20die?= =?UTF-8?q?=20Sitzung=20des=20Ger=C3=A4ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync-Clients schicken mit jeder Anfrage ihr Sitzungs-Cookie mit. Nach dem Widerruf eines App-Passworts in den Einstellungen synchronisierte ein solcher Client unbegrenzt weiter: widerrufenes App-Passwort + Cookie 207, nur Cookie 207 (belegt an 11.0.40 und dev-503-sitzung). Sitzungen, die ein Client selbst angemeldet hat (Vermerk AUTHENTICATED_TO_DAV_BACKEND), prüfte OC_App::loadApps() nie gegen die Anmeldung, mit der sie begannen; fehlte ihr Sitzungstoken, legte es dasselbe einfach neu an. Darum wirkte auch „Sitzung entfernen“ unter „Sitzungen“ nicht, und nach dem Widerruf einer Desktop-Freigabe (OAuth2) galt die Sitzung mit dem Cookie allein weiter, auch für die Weboberfläche. Jede Anfrage verlängerte sie. owncloud/core master verhält sich gleich (loadApps, validateSession, TokenAuthModule). Session::validateDavSession() prüft solche Sitzungen jetzt vor jeder Anfrage, wie validateSession() die des Browsers: - App-Passwort ('app_password' in der Sitzung): Das Token muss bestehen und dem Nutzer gehören, sonst endet die Sitzung – auch bei Anfragen nur mit Cookie. - Sitzungstoken: Hatte die Sitzung schon eines (neuer Vermerk session_token_created aus createSessionToken()), muss es bestehen. Fehlt es noch, entsteht es wie bisher erst mit der zweiten Anfrage, damit Clients ohne Cookies nicht je Anfrage ein Token anlegen. Die letzte Aktivität des Tokens wird höchstens einmal je Minute fortgeschrieben; sonst löschte der Aufräumjob die Tokens laufender Sitzungen nach session_lifetime. - Anmeldemodul (OAuth2, OpenID Connect): tryAuthModuleLogin() merkt sich Modul und SHA-256 des Authorization-Kopfs. Einen anderen Kopf – auch keinen, also nur das Cookie – prüft das Modul sofort, denselben nach core/last_check_timeout Minuten (last_check am Sitzungstoken, wie die Kennwortprüfung). Bestätigt es den Nutzer der Sitzung nicht, endet sie. - Sitzungen von vor dieser Änderung: Ihr Token bekommt den Vermerk mit der ersten Anfrage. Schicken sie einen Bearer, wird das Modul vermerkt, das ihren Nutzer bestätigt; schicken sie ohne App-Passwort nur das Cookie, melden sie sich einmal neu an. Eine beendete Sitzung wird abgemeldet und verworfen (Sitzungstoken gelöscht, Sitzung geleert); das Protokoll meldet auf Stufe info „Client session of ended: “. Browser-Sitzungen laufen unverändert über validateSession(). Bewusst nicht sofort verworfen wird eine OAuth2-Sitzung bei der ersten WebDAV-Anfrage mit widerrufenem Bearer: Ein abgelaufenes Access-Token sieht der Kern genauso, und dann risse jedes stündliche Ablaufen die Sitzung ab (Neuanmeldung je Client und Stunde, bei parallelen Anfragen genau die Last, die dev-503-sitzung entschärft). Die Anfrage selbst weist die OAuth2-App wie bisher sofort ab (401); OCS und Weboberfläche prüfen den Kopf ebenfalls bei jeder Anfrage und verwerfen die Sitzung dabei. Mit nur dem Cookie endet sie sofort, sonst nach core/last_check_timeout. Je Anfrage liest das wie bisher das Sitzungstoken aus der Datenbank, bei App-Passwörtern dazu dessen Token, das die Basic-Anmeldung derselben Anfrage ohnehin lädt (gemerkt im Provider). Geschrieben wird nur bei Änderungen. TokenAuthModule, BasicAuthModule und das DAV-Backend bleiben unverändert: Bei geprüfter Sitzung bestätigt der Rückgriff des TokenAuthModule auf das Sitzungstoken nur noch deren eigenen Nutzer. Tests: SessionTest deckt App-Passwort (gültig, widerrufen, fremd), Sitzungstoken (entfernt, erst anlegen, vorhanden, parallel angelegt, fremd), gesperrten Nutzer, Kennwort-Sitzung, Anmeldemodul (gleicher Kopf, nach Rewrite, nur Cookie, Basic, erneuert, fremder Nutzer, Frist fällig und gerade nicht, widerrufen, Modul weg), Sitzungen von vor der Änderung und den Vermerk in tryAuthModuleLogin() ab. Ohne die Änderung scheitern alle 29 neuen Tests, mit ihr sind alle 82 grün; 27 Mutationen der Regeln, alle erkannt. Volle Suite: dieselben 44 umgebungsbedingten Fehler (Vorschau) wie dev-503-sitzung, keine neuen. php-cs-fixer, phpstan und phan ohne Befund. Nachgestellt an einer eigenen Instanz (11.0.40 + dev-503-sitzung, Redis mit Sitzungssperre wie testoo, oauth2 0.10.0), vorher → nachher: - App-Passwort widerrufen: App-Passwort + Cookie 207 → 401, nur Cookie 207 → 401; Sitzung und Sitzungstoken gelöscht - Desktop-Freigabe widerrufen: Bearer + Cookie 401 → 401, nur Cookie 207 → 401 - Sitzung unter „Sitzungen“ entfernt (App-Passwort, Bearer, Kennwort): nur Cookie 207 → 401 (vorher legte die Anfrage das Token neu an); mit gültiger Anmeldung wieder 207 - Weboberfläche mit dem Cookie: nach Widerruf des App-Passworts 200 → 401, OAuth2-Sitzung nur mit Cookie 200 → 401 - nur Cookie zu einer OAuth2-Sitzung ohne Widerruf: 207 → 401 (gewollt; Desktop- und Android-Client senden den Bearer mit jeder Anfrage) - erneuertes Access-Token mit altem Cookie: 207, Sitzung bleibt - OCS mit widerrufenem Bearer: wie bisher 401 - 20 parallele PROPFIND (Bearer, App-Passwort, Kennwort), auch mit Redis +1,2 ms: alle 207, kein 503, Laufzeiten wie dev-503-sitzung (Bearer p50 0,61 s und p95 0,83 s in beiden Ständen, App-Passwort p95 0,90 → 0,91 s, Kennwort p95 1,29 → 1,28 s) - Playwright: Anmeldung, Weboberfläche, öffentlicher Link, Gast unverändert grün --- .../unreleased/revoke-ends-client-sessions | 39 ++ docs/administration/troubleshooting.md | 26 ++ lib/private/User/Session.php | 282 +++++++++++++ lib/private/legacy/app.php | 20 +- tests/lib/User/SessionTest.php | 385 ++++++++++++++++++ 5 files changed, 735 insertions(+), 17 deletions(-) create mode 100644 changelog/unreleased/revoke-ends-client-sessions diff --git a/changelog/unreleased/revoke-ends-client-sessions b/changelog/unreleased/revoke-ends-client-sessions new file mode 100644 index 0000000000..b4f48d2a42 --- /dev/null +++ b/changelog/unreleased/revoke-ends-client-sessions @@ -0,0 +1,39 @@ +Bugfix: Revoked access ends the session of a sync client + +Sync clients send their session cookie along with every request. After an app +password was revoked in the personal settings, a client that used it kept +syncing: requests with the revoked app password and the cookie, and requests +with the cookie alone, were answered with 207. Sessions started by a client +itself (WebDAV, HTTP Basic, OAuth2) were never checked against the credentials +they were started with, and every request extended them. Removing such a +session under "Sessions" in the settings had no effect either, its session +token was simply created again with the next request. A session started with +an OAuth2 token went on accepting requests with the cookie alone after the +desktop authorization was revoked. + +These sessions are now checked before every request, like browser sessions: + +- App password: the session ends with the next request after the app password + was revoked, also when the client only sends the cookie. +- Session removed in the settings: the session ends with the next request. A + client with valid credentials signs in again and gets a new session. +- OAuth2 and OpenID Connect: every request has to carry the Authorization + header that the authentication module confirmed. A different header, or none + at all, is checked by the module at once, the same header again after + core/last_check_timeout minutes (default 5). As before, the module itself + rejects WebDAV requests with a revoked token right away. + +A session that is no longer valid is logged out and its session token is +deleted; the request continues without a login and gets 401 unless it carries +valid credentials. Requests that send only the cookie of a session started with +an OAuth2 token are no longer accepted; the desktop and mobile clients send +their token with every request. Sessions started before the update are taken +over with their first request; if that request sends only the cookie and the +session was not started with an app password, the client signs in once more. + +The check reads the session token from the database as before, for app +passwords also the app password token, which the HTTP Basic login of the same +request loads anyway. The session and the token are written only when +something changes, the last activity of the token at most once a minute. +Browser sessions, public links, guests and the early release of the session +lock for sync clients are unchanged. diff --git a/docs/administration/troubleshooting.md b/docs/administration/troubleshooting.md index 381b5fb881..c0a3a3018e 100644 --- a/docs/administration/troubleshooting.md +++ b/docs/administration/troubleshooting.md @@ -101,6 +101,32 @@ Ursachen: redis-cli -h -n 1 DEL '_LOCK' ``` +## Client nach Widerruf abgemeldet + +Sync-Clients schicken mit jeder Anfrage ihr Sitzungs-Cookie mit. Die Sitzung +gilt nur, solange die Anmeldung gilt, mit der sie begann; der Server prüft das +vor jeder Anfrage: + +- **App-Passwort entzogen** (Persönlich → Sicherheit → App-Passwörter / Token): + Die nächste Anfrage des Geräts bekommt 401, auch wenn es nur das Cookie + schickt. Der Client muss mit einem neuen App-Passwort eingerichtet werden. +- **Sitzung unter „Sitzungen“ entfernt**: Die Sitzung endet mit der nächsten + Anfrage. Hat das Gerät noch gültige Zugangsdaten (App-Passwort, OAuth2-Token, + Kennwort), meldet es sich damit sofort neu an. Um ein Gerät auszusperren, + dessen Zugang entziehen, nicht nur die Sitzung. +- **Desktop-Freigabe (OAuth2) widerrufen**: WebDAV- und OCS-Anfragen mit dem + Token werden sofort abgewiesen. Die Sitzung selbst endet mit der ersten + Anfrage ohne das Token (etwa nur mit Cookie), sonst mit der ersten Anfrage, + die mehr als `last_check_timeout` Minuten (App-Wert `core`, Vorgabe 5) nach + der letzten Prüfung des Tokens kommt. + +Eine so beendete Sitzung steht mit Stufe info im Protokoll: +„Client session of `` ended: ``“. Anfragen, die zu einer per +OAuth2 begonnenen Sitzung nur das Cookie ohne Token schicken, werden nicht mehr +angenommen; Desktop- und Mobil-Clients senden das Token immer mit. Sitzungen, +die vor dem Update begannen und danach ohne App-Passwort nur das Cookie +schicken, müssen sich einmal neu anmelden. + ## JavaScript-Fehler: jQuery ist nicht definiert Ursache ist meist ein unvollständiger Build. Bei owncloud.online müssen Composer und `make` im Release-Prozess laufen. diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index ee4a4a2bd9..2ba4681cda 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -94,6 +94,21 @@ */ class Session implements IUserSession, Emitter { use EventEmitterTrait; + + /** + * Sitzungsschlüssel: Diese Sitzung hatte schon ein Sitzungstoken (oc_authtoken). + * Fehlt es danach, wurde es entfernt, und die Sitzung gilt nicht mehr + * (siehe validateDavSession()). + */ + private const SESSION_TOKEN_CREATED = 'session_token_created'; + + /** + * Sitzungsschlüssel: Ein Anmeldemodul (etwa OAuth2) hat die Sitzung angemeldet. + * Wert: ['class' => Klasse des Moduls, 'credential' => SHA-256 des + * Authorization-Kopfs, den das Modul zuletzt bestätigt hat]. + */ + private const AUTH_MODULE = 'auth_module'; + /** @var IUserManager | PublicEmitter $manager */ private $manager; @@ -281,6 +296,265 @@ public function validateSession() { } } + /** + * Prüft vor jeder Anfrage eine Sitzung, die ein Client selbst angemeldet hat + * (WebDAV, HTTP-Basic oder ein Anmeldemodul wie OAuth2; vermerkt unter + * Auth::DAV_AUTHENTICATED). + * + * Eine solche Sitzung gilt nur, solange gilt, womit sie angemeldet wurde: + * - App-Passwort: Es muss noch bestehen. Wird es widerrufen, endet die Sitzung + * mit der nächsten Anfrage, auch wenn der Client nur das Cookie schickt. + * - Sitzungstoken: Hatte die Sitzung schon eines, muss es noch bestehen (etwa + * unter „Sitzungen“ entfernt). Fehlt es noch, legt diese Anfrage es an – wie + * bisher erst, wenn der Client das Cookie zurückschickt, damit Clients ohne + * Cookies nicht mit jeder Anfrage ein Token erzeugen. + * - Anmeldemodul (Bearer): Jede Anfrage muss den Authorization-Kopf tragen, + * den das Modul zuletzt bestätigt hat. Einen anderen – auch gar keinen – + * prüft das Modul sofort, denselben spätestens nach core/last_check_timeout + * Minuten wieder (wie die Kennwortprüfung). Für WebDAV prüft das Modul den + * Bearer ohnehin bei jeder Anfrage. Kommt ein solcher Kopf zu einer Sitzung + * ohne diesen Vermerk (begonnen vor dieser Prüfung), muss ein Modul ihren + * Nutzer bestätigen und wird dann vermerkt. + * - Eine Sitzung von vor dieser Prüfung ohne App-Passwort, deren erste Anfrage + * danach nur das Cookie schickt, meldet sich einmal neu an: Ob sie mit + * Kennwort oder Bearer begann, ist nicht bekannt. + * Gilt sie nicht mehr, wird die Sitzung abgemeldet und verworfen. Die Anfrage + * läuft ohne Anmeldung weiter und kann sich mit gültigen Zugangsdaten neu + * anmelden. + * + * Aus der Datenbank liest das je Anfrage wie bisher das Sitzungstoken, bei + * App-Passwörtern auch dessen Token; das lädt die Anmeldung mit HTTP-Basic in + * derselben Anfrage ohnehin. + * + * @param IRequest $request + */ + public function validateDavSession(IRequest $request) { + $sessionUser = $this->getUser(); + if ($sessionUser === null) { + return; + } + if (!$sessionUser->isEnabled()) { + $this->endDavSession($sessionUser, 'user disabled'); + return; + } + + $appPassword = $this->session->get('app_password'); + if ($appPassword !== null && !$this->isTokenOfUser($appPassword, $sessionUser)) { + $this->endDavSession($sessionUser, 'app password no longer valid'); + return; + } + + try { + $sessionId = $this->session->getId(); + } catch (SessionNotAvailableException $ex) { + return; + } + try { + $sessionToken = $this->tokenProvider->getToken($sessionId); + } catch (InvalidTokenException $ex) { + $sessionToken = null; + } + $beganBeforeCheck = false; + if ($sessionToken === null) { + if ($this->session->exists(self::SESSION_TOKEN_CREATED)) { + $this->endDavSession($sessionUser, 'session token removed'); + return; + } + $this->createSessionToken($request, $sessionUser->getUID(), $this->getLoginName(), $request->server['PHP_AUTH_PW'] ?? null); + } elseif ($sessionToken->getUID() !== $sessionUser->getUID()) { + $this->endDavSession($sessionUser, 'session token of another user'); + return; + } elseif (!$this->session->exists(self::SESSION_TOKEN_CREATED)) { + // Sitzung von vor dieser Prüfung: ab jetzt gilt sie wie jede neue + $this->session->set(self::SESSION_TOKEN_CREATED, true); + $beganBeforeCheck = true; + } + + $problem = $this->checkClientCredential($sessionUser, $request, $sessionToken, $appPassword !== null, $beganBeforeCheck); + if ($problem !== null) { + $this->endDavSession($sessionUser, $problem); + return; + } + + if ($sessionToken !== null) { + $this->tokenProvider->updateTokenActivity($sessionToken); + } + } + + /** + * Passen die Zugangsdaten der Anfrage zu der Anmeldung, mit der die Sitzung + * begann (siehe validateDavSession())? + * + * @param IUser $sessionUser + * @param IRequest $request + * @param IToken|null $sessionToken + * @param bool $hasAppPassword die Sitzung begann mit einem App-Passwort + * @param bool $beganBeforeCheck die Sitzung begann vor dieser Prüfung + * @return string|null warum die Sitzung nicht mehr gilt, null wenn sie gilt + */ + private function checkClientCredential(IUser $sessionUser, IRequest $request, ?IToken $sessionToken, $hasAppPassword, $beganBeforeCheck) { + $authModule = $this->session->get(self::AUTH_MODULE); + if ($authModule !== null) { + return $this->isAuthModuleCredentialValid($authModule, $sessionUser, $request, $sessionToken) + ? null : 'credential not confirmed by the authentication module'; + } + if ($hasAppPassword) { + return null; + } + if (self::sendsModuleCredential($request)) { + return $this->adoptAuthModule($sessionUser, $request) + ? null : 'credential not confirmed by any authentication module'; + } + if ($beganBeforeCheck && self::getAuthorizationHeader($request) === '' && !isset($request->server['PHP_AUTH_USER'])) { + // Ob eine Sitzung von vor dieser Prüfung mit Kennwort oder Bearer + // begann, ist unbekannt; nur mit Cookie ließe sich ein widerrufener + // Bearer nie erkennen. Einmal neu anmelden. + return 'session from before the check without credentials'; + } + return null; + } + + /** + * @param string $token + * @param IUser $user + * @return bool true, wenn das Token besteht und dem Nutzer gehört + */ + private function isTokenOfUser($token, IUser $user) { + try { + return $this->tokenProvider->getToken($token)->getUID() === $user->getUID(); + } catch (InvalidTokenException $ex) { + return false; + } + } + + /** + * Bestätigt das Anmeldemodul, das die Sitzung angemeldet hat, noch ihren + * Nutzer? Gefragt wird es nur, wenn der Authorization-Kopf ein anderer ist als + * bei der letzten Bestätigung (auch: fehlt) oder diese länger als + * core/last_check_timeout Minuten her ist; vermerkt wird sie wie die + * Kennwortprüfung am Sitzungstoken. + * + * @param mixed $authModule Wert von self::AUTH_MODULE + * @param IUser $sessionUser + * @param IRequest $request + * @param IToken|null $sessionToken null, wenn diese Anfrage es eben erst angelegt hat + * @return bool + */ + private function isAuthModuleCredentialValid($authModule, IUser $sessionUser, IRequest $request, ?IToken $sessionToken) { + $class = \is_array($authModule) ? ($authModule['class'] ?? null) : null; + $confirmed = \is_array($authModule) ? ($authModule['credential'] ?? null) : null; + $credential = self::getCredentialFingerprint($request); + $changed = !\is_string($confirmed) || !\hash_equals($confirmed, $credential); + $now = $this->timeFactory->getTime(); + $checkTimeout = 60 * (int)$this->config->getAppValue('core', 'last_check_timeout', 5); + if (!$changed && ($sessionToken === null || $sessionToken->getLastCheck() > $now - $checkTimeout)) { + return true; + } + + $user = null; + foreach ($this->getAuthModules(false) as $module) { + if (\get_class($module) !== $class) { + continue; + } + try { + $user = $module->auth($request); + } catch (Exception $ex) { + $user = null; + } + break; + } + if ($user === null || $user->getUID() !== $sessionUser->getUID()) { + return false; + } + + if ($changed) { + $this->session->set(self::AUTH_MODULE, ['class' => $class, 'credential' => $credential]); + } + if ($sessionToken !== null) { + $sessionToken->setLastCheck($now); + $this->tokenProvider->updateToken($sessionToken); + } + return true; + } + + /** + * Einen solchen Kopf (etwa Bearer) prüft nur ein Anmeldemodul, die Sitzung hat + * aber keines vermerkt – sie begann vor dieser Prüfung, oder der Client + * wechselt das Verfahren. Bestätigt ein Modul den Nutzer der Sitzung, wird es + * vermerkt wie bei tryAuthModuleLogin(); sonst gilt die Sitzung nicht. + * + * @param IUser $sessionUser + * @param IRequest $request + * @return bool + */ + private function adoptAuthModule(IUser $sessionUser, IRequest $request) { + foreach ($this->getAuthModules(false) as $module) { + try { + $user = $module->auth($request); + } catch (Exception $ex) { + return false; + } + if ($user === null) { + continue; + } + if ($user->getUID() !== $sessionUser->getUID()) { + return false; + } + $this->session->set(self::AUTH_MODULE, [ + 'class' => \get_class($module), + 'credential' => self::getCredentialFingerprint($request), + ]); + return true; + } + return false; + } + + /** + * Fingerabdruck der Zugangsdaten, die ein Anmeldemodul prüft: des + * Authorization-Kopfs, auch wenn Apache ihn nur nach einem Rewrite weiterreicht. + * + * @param IRequest $request + * @return string + */ + private static function getCredentialFingerprint(IRequest $request) { + return \hash('sha256', self::getAuthorizationHeader($request)); + } + + /** + * Trägt die Anfrage Zugangsdaten, die nur ein Anmeldemodul prüft (etwa + * Bearer), also weder keine noch HTTP-Basic? + * + * @param IRequest $request + * @return bool + */ + private static function sendsModuleCredential(IRequest $request) { + $authorization = self::getAuthorizationHeader($request); + return $authorization !== '' && \stripos($authorization, 'Basic ') !== 0; + } + + /** + * @param IRequest $request + * @return string + */ + private static function getAuthorizationHeader(IRequest $request) { + return $request->getHeader('Authorization') ?? $request->server['REDIRECT_HTTP_AUTHORIZATION'] ?? ''; + } + + /** + * Meldet eine Client-Sitzung ab, deren Anmeldung nicht mehr gilt, und + * verwirft sie samt Sitzungstoken. + * + * @param IUser $user + * @param string $reason + */ + private function endDavSession(IUser $user, $reason) { + $this->logger->info( + 'Client session of {uid} ended: {reason}', + ['app' => __METHOD__, 'uid' => $user->getUID(), 'reason' => $reason] + ); + $this->logout(); + } + /** * Checks whether the user is logged in * @@ -690,6 +964,7 @@ public function createSessionToken(IRequest $request, $uid, $loginName, $passwor // differs — a session id reused for another user — fall through and // let generateToken run; a collision there keeps the prior behaviour. $this->tokenProvider->updateTokenActivity($existingToken); + $this->session->set(self::SESSION_TOKEN_CREATED, true); return true; } } catch (InvalidTokenException $ex) { @@ -697,6 +972,7 @@ public function createSessionToken(IRequest $request, $uid, $loginName, $passwor } $pwd = $this->getPassword($password); $this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name); + $this->session->set(self::SESSION_TOKEN_CREATED, true); return true; } catch (SessionNotAvailableException $ex) { // This can happen with OCC, where a memory session is used @@ -715,6 +991,7 @@ public function createSessionToken(IRequest $request, $uid, $loginName, $passwor 'reusing the existing token.', ['app' => __METHOD__] ); + $this->session->set(self::SESSION_TOKEN_CREATED, true); return true; // the session already has an auth token, go ahead. } } @@ -952,6 +1229,11 @@ public function tryAuthModuleLogin(IRequest $request) { if ($loginOk) { $this->createSessionToken($request, $uid, $uid, $password); $this->session->set(Auth::DAV_AUTHENTICATED, $uid); + // Woran validateDavSession() die Sitzung später misst + $this->session->set(self::AUTH_MODULE, [ + 'class' => \get_class($authModule), + 'credential' => self::getCredentialFingerprint($request), + ]); } return $loginOk; } diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php index a7da1e8660..375bf32916 100644 --- a/lib/private/legacy/app.php +++ b/lib/private/legacy/app.php @@ -148,23 +148,9 @@ public static function loadApps($types = null) { if ($davUser === null) { $session->validateSession(); } else { - /** @var \OC\Authentication\Token\DefaultTokenProvider $tokenProvider */ - $tokenProvider = \OC::$server->query('\OC\Authentication\Token\DefaultTokenProvider'); - $token = null; - try { - $token = $tokenProvider->getToken($session->getSession()->getId()); - } catch (\Exception $ex) { - $password = null; - if (isset($_SERVER['PHP_AUTH_PW'])) { - $password = $_SERVER['PHP_AUTH_PW']; - } - - $session->createSessionToken($request, $session->getUser()->getUID(), $session->getLoginName(), $password); - } - - if ($token) { - $tokenProvider->updateToken($token); - } + // Von einem Client selbst angemeldet: Die Sitzung endet mit dem + // App-Passwort, Sitzungstoken oder Bearer, mit dem sie begann. + $session->validateDavSession($request); } } } diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php index 65e98a14a9..55f946fac8 100644 --- a/tests/lib/User/SessionTest.php +++ b/tests/lib/User/SessionTest.php @@ -15,6 +15,7 @@ namespace Test\User; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use OC\AppFramework\Http\Request; use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException; @@ -2076,4 +2077,388 @@ public function testFailedLoginUserDisabled() { self::invokePrivate($userSession, 'loginUser', [$iUser, 'bar', 'authModuleClassname']); } + + /** + * Sitzung, die ein Client selbst angemeldet hat (WebDAV), mit Kennung + * 'sessionid'; logout() und die Anmeldemodule sind ersetzt. + * + * @param array $values zusätzliche Sitzungswerte + * @param IAuthModule[] $modules + * @param bool $enabled + * @return array [Session (Mock), Sitzungsspeicher] + */ + private function createDavSession(array $values, array $modules = [], $enabled = true) { + $store = new class extends Memory { + public function getId() { + return 'sessionid'; + } + }; + $values += [ + 'user_id' => 'user1', + 'loginname' => 'User1', + 'AUTHENTICATED_TO_DAV_BACKEND' => 'user1', + ]; + foreach ($values as $key => $value) { + $store->set($key, $value); + } + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user1'); + $user->method('isEnabled')->willReturn($enabled); + $manager = $this->createMock(Manager::class); + $manager->method('get')->willReturnCallback(function ($uid) use ($user) { + return $uid === 'user1' ? $user : null; + }); + $this->config->method('getAppValue')->willReturnMap([['core', 'last_check_timeout', 5, '5']]); + + /** @var Session | \PHPUnit\Framework\MockObject\MockObject $userSession */ + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$manager, $store, $this->timeFactory, $this->tokenProvider, $this->config, $this->logger, $this->serviceLoader, $this->userSyncService, $this->eventDispatcher]) + ->onlyMethods(['logout', 'getAuthModules']) + ->getMock(); + $userSession->method('getAuthModules')->willReturn($modules); + return [$userSession, $store]; + } + + /** + * @param string $uid + * @param int $lastCheck + * @return DefaultToken + */ + private function createDavToken($uid, $lastCheck = 10000) { + $token = new DefaultToken(); + $token->setUid($uid); + $token->setLastCheck($lastCheck); + $token->setLastActivity(10000); + return $token; + } + + /** + * @param array $server + * @return Request + */ + private function createDavRequest(array $server = []) { + return new Request( + ['server' => $server + ['HTTP_USER_AGENT' => 'mirall']], + $this->createMock(ISecureRandom::class), + $this->createMock(IConfig::class), + $this->createMock(CsrfTokenManager::class) + ); + } + + /** + * Tokens nach Kennung: Fehlt eine, gilt das Token als nicht vorhanden. + * + * @param IToken[] $tokens + */ + private function withTokens(array $tokens) { + $this->tokenProvider->method('getToken')->willReturnCallback(function ($id) use ($tokens) { + if (!isset($tokens[$id])) { + throw new InvalidTokenException(); + } + return $tokens[$id]; + }); + } + + public function testValidateDavSessionKeepsAppPasswordSession() { + [$userSession, $store] = $this->createDavSession(['app_password' => 'APPPW', 'session_token_created' => true]); + $sessionToken = $this->createDavToken('user1'); + $this->withTokens(['APPPW' => $this->createDavToken('user1'), 'sessionid' => $sessionToken]); + $this->tokenProvider->expects($this->once())->method('updateTokenActivity')->with($sessionToken); + $userSession->expects($this->never())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest()); + $this->assertSame('APPPW', $store->get('app_password')); + } + + public function providesInvalidAppPasswords() { + return [ + 'widerrufen' => [[]], + 'Token eines anderen Nutzers' => [['APPPW' => 'user2']], + ]; + } + + /** + * Widerrufenes App-Passwort: Die Sitzung endet, auch wenn der Client nur das + * Cookie schickt und das Sitzungstoken noch besteht. + * + * @dataProvider providesInvalidAppPasswords + * @param array $owners Token => Nutzer + */ + public function testValidateDavSessionEndsWhenAppPasswordIsInvalid(array $owners) { + [$userSession] = $this->createDavSession(['app_password' => 'APPPW', 'session_token_created' => true]); + $tokens = ['sessionid' => $this->createDavToken('user1')]; + foreach ($owners as $id => $uid) { + $tokens[$id] = $this->createDavToken($uid); + } + $this->withTokens($tokens); + $this->logger->expects($this->once())->method('info') + ->with($this->stringContains('ended'), $this->callback(function ($context) { + return $context['reason'] === 'app password no longer valid'; + })); + $userSession->expects($this->once())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest()); + } + + /** + * Sitzung unter „Sitzungen“ entfernt: Das Token wird nicht neu angelegt. + */ + public function testValidateDavSessionEndsWhenSessionTokenWasRemoved() { + [$userSession] = $this->createDavSession(['session_token_created' => true]); + $this->withTokens([]); + $this->tokenProvider->expects($this->never())->method('generateToken'); + $userSession->expects($this->once())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest(['PHP_AUTH_USER' => 'user1', 'PHP_AUTH_PW' => 'secret'])); + } + + /** + * Zweite Anfrage einer Sitzung: Das Token entsteht wie bisher erst jetzt, + * mit dem Kennwort der Anfrage, und die Sitzung merkt sich das. + */ + public function testValidateDavSessionCreatesSessionTokenOnce() { + [$userSession, $store] = $this->createDavSession([]); + $this->withTokens([]); + $this->tokenProvider->expects($this->once())->method('generateToken') + ->with('sessionid', 'user1', 'User1', 'secret', 'mirall'); + $userSession->expects($this->never())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest(['PHP_AUTH_USER' => 'user1', 'PHP_AUTH_PW' => 'secret'])); + $this->assertTrue($store->get('session_token_created')); + } + + public function providesExistingSessionTokens() { + return [ + 'Token der Sitzung besteht schon' => [false], + 'parallel angelegt (Unique-Verletzung)' => [true], + ]; + } + + /** + * Auch wenn das Token schon besteht, merkt sich die Sitzung, dass sie eines hat. + * + * @dataProvider providesExistingSessionTokens + * @param bool $concurrent + */ + public function testCreateSessionTokenMarksSessionWithExistingToken($concurrent) { + $store = new class extends Memory { + public function getId() { + return 'sessionid'; + } + }; + $manager = $this->createMock(Manager::class); + $manager->method('get')->willReturn($this->createMock(IUser::class)); + $userSession = new Session($manager, $store, $this->timeFactory, $this->tokenProvider, $this->config, $this->logger, $this->serviceLoader, $this->userSyncService, $this->eventDispatcher); + if ($concurrent) { + $this->withTokens([]); + $this->tokenProvider->expects($this->once())->method('generateToken') + ->willThrowException($this->createMock(UniqueConstraintViolationException::class)); + } else { + $this->withTokens(['sessionid' => $this->createDavToken('user1')]); + $this->tokenProvider->expects($this->never())->method('generateToken'); + } + + $this->assertTrue($userSession->createSessionToken($this->createDavRequest(), 'user1', 'User1')); + $this->assertTrue($store->get('session_token_created')); + } + + public function providesSessionsFromBeforeTheCheck() { + return [ + 'App-Passwort, nur Cookie' => [['app_password' => 'APPPW'], [], false], + 'Kennwort über Basic' => [[], ['HTTP_AUTHORIZATION' => 'Basic dXNlcjE6c2VjcmV0'], false], + 'Kennwort über Basic ohne Authorization-Kopf (mod_php)' => [[], ['PHP_AUTH_USER' => 'user1', 'PHP_AUTH_PW' => 'secret'], false], + 'nur Cookie, weder App-Passwort noch Kennwort' => [[], [], true], + ]; + } + + /** + * Sitzung von vor dieser Prüfung (Token ohne Vermerk): Sie behält ihr Token + * und gilt ab jetzt wie jede neue. Woher sie stammt, zeigt erst die Anfrage; + * nur mit Cookie und ohne App-Passwort ist das unbekannt – einmal neu anmelden. + * + * @dataProvider providesSessionsFromBeforeTheCheck + * @param array $values + * @param array $server + * @param bool $ends + */ + public function testValidateDavSessionOfSessionFromBeforeTheCheck(array $values, array $server, $ends) { + [$userSession, $store] = $this->createDavSession($values); + $this->withTokens(['APPPW' => $this->createDavToken('user1'), 'sessionid' => $this->createDavToken('user1')]); + $this->tokenProvider->expects($this->never())->method('generateToken'); + $userSession->expects($ends ? $this->once() : $this->never())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest($server)); + $this->assertTrue($store->get('session_token_created')); + } + + public function testValidateDavSessionEndsWhenSessionTokenBelongsToOtherUser() { + [$userSession] = $this->createDavSession(['session_token_created' => true]); + $this->withTokens(['sessionid' => $this->createDavToken('user2')]); + $userSession->expects($this->once())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest()); + } + + public function testValidateDavSessionEndsForDisabledUser() { + [$userSession] = $this->createDavSession(['session_token_created' => true], [], false); + $this->withTokens(['sessionid' => $this->createDavToken('user1')]); + $userSession->expects($this->once())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest()); + } + + /** + * Kennwort über HTTP-Basic: kein Anmeldemodul beteiligt, die Sitzung bleibt. + */ + public function testValidateDavSessionKeepsPasswordSession() { + $module = $this->createMock(IAuthModule::class); + $module->expects($this->never())->method('auth'); + [$userSession] = $this->createDavSession(['session_token_created' => true], [$module]); + $this->withTokens(['sessionid' => $this->createDavToken('user1')]); + $userSession->expects($this->never())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest(['HTTP_AUTHORIZATION' => 'Basic dXNlcjE6c2VjcmV0'])); + } + + public function providesModulesForUnmarkedSessions() { + return [ + 'zweites Modul bestätigt den Nutzer' => ['user1', false], + 'kein Modul kennt den Kopf' => [null, true], + 'Modul nennt einen anderen Nutzer' => ['user2', true], + 'Modul lehnt ab' => [new \OC\User\LoginException('Invalid token'), true], + ]; + } + + /** + * Bearer an einer Sitzung ohne Vermerk eines Anmeldemoduls (begonnen vor + * dieser Prüfung): Bestätigt ein Modul den Nutzer, wird es vermerkt, sonst + * endet die Sitzung. + * + * @dataProvider providesModulesForUnmarkedSessions + * @param string|\Exception|null $moduleResult + * @param bool $ends + */ + public function testValidateDavSessionOfUnmarkedSessionWithBearer($moduleResult, $ends) { + $first = $this->createMock(IAuthModule::class); + $first->method('auth')->willReturn(null); + $second = $this->createMock(IAuthModule::class); + $auth = $second->expects($this->once())->method('auth'); + if ($moduleResult instanceof \Exception) { + $auth->willThrowException($moduleResult); + } elseif ($moduleResult !== null) { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn($moduleResult); + $auth->willReturn($user); + } else { + $auth->willReturn(null); + } + [$userSession, $store] = $this->createDavSession(['session_token_created' => true], [$first, $second]); + $this->withTokens(['sessionid' => $this->createDavToken('user1')]); + $userSession->expects($ends ? $this->once() : $this->never())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest(['HTTP_AUTHORIZATION' => 'Bearer abc'])); + if (!$ends) { + $this->assertSame( + ['class' => \get_class($second), 'credential' => \hash('sha256', 'Bearer abc')], + $store->get('auth_module') + ); + } + } + + public function providesAuthModuleRequests() { + return [ + 'derselbe Bearer, eben geprüft' => [['HTTP_AUTHORIZATION' => 'Bearer abc'], 10000, false, null, false], + 'derselbe Bearer nach Rewrite, eben geprüft' => [['REDIRECT_HTTP_AUTHORIZATION' => 'Bearer abc'], 10000, false, null, false], + 'nur Cookie' => [[], 10000, true, null, true], + 'Basic statt Bearer' => [['HTTP_AUTHORIZATION' => 'Basic dXNlcjE6c2VjcmV0'], 10000, true, null, true], + 'erneuerter Bearer' => [['HTTP_AUTHORIZATION' => 'Bearer neu'], 10000, true, 'user1', false], + 'Bearer eines anderen Nutzers' => [['HTTP_AUTHORIZATION' => 'Bearer fremd'], 10000, true, 'user2', true], + 'derselbe Bearer, Prüfung fällig, gilt noch' => [['HTTP_AUTHORIZATION' => 'Bearer abc'], 10000 - 301, true, 'user1', false], + 'derselbe Bearer, Prüfung fällig, widerrufen' => [['HTTP_AUTHORIZATION' => 'Bearer abc'], 10000 - 301, true, new \OC\User\LoginException('Invalid token'), true], + 'derselbe Bearer, Prüfung gerade noch nicht fällig' => [['HTTP_AUTHORIZATION' => 'Bearer abc'], 10000 - 299, false, null, false], + ]; + } + + /** + * Sitzung eines Anmeldemoduls (OAuth2): Jede Anfrage muss den bestätigten + * Authorization-Kopf tragen; ein anderer wird sofort geprüft, derselbe nach + * last_check_timeout (5 Minuten). + * + * @dataProvider providesAuthModuleRequests + * @param array $server + * @param int $lastCheck + * @param bool $asksModule + * @param string|\Exception|null $moduleResult Nutzer, Ausnahme oder null + * @param bool $ends + */ + public function testValidateDavSessionOfAuthModule(array $server, $lastCheck, $asksModule, $moduleResult, $ends) { + $module = $this->createMock(IAuthModule::class); + $auth = $module->expects($asksModule ? $this->once() : $this->never())->method('auth'); + if ($moduleResult instanceof \Exception) { + $auth->willThrowException($moduleResult); + } elseif ($moduleResult !== null) { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn($moduleResult); + $auth->willReturn($user); + } else { + $auth->willReturn(null); + } + $authModule = ['class' => \get_class($module), 'credential' => \hash('sha256', 'Bearer abc')]; + [$userSession, $store] = $this->createDavSession(['session_token_created' => true, 'auth_module' => $authModule], [$module]); + $sessionToken = $this->createDavToken('user1', $lastCheck); + $this->withTokens(['sessionid' => $sessionToken]); + $userSession->expects($ends ? $this->once() : $this->never())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest($server)); + + if (!$ends && $asksModule) { + $authorization = $server['HTTP_AUTHORIZATION'] ?? ''; + $this->assertSame(\hash('sha256', $authorization), $store->get('auth_module')['credential']); + $this->assertSame(10000, $sessionToken->getLastCheck()); + } elseif (!$ends) { + $this->assertSame($authModule, $store->get('auth_module')); + $this->assertSame($lastCheck, $sessionToken->getLastCheck()); + } + } + + /** + * Ist die App des Anmeldemoduls nicht mehr da, lässt sich der Kopf nicht + * prüfen: Die Sitzung endet. + */ + public function testValidateDavSessionEndsWhenAuthModuleIsGone() { + $authModule = ['class' => 'OCA\Gone\AuthModule', 'credential' => \hash('sha256', 'Bearer abc')]; + [$userSession] = $this->createDavSession(['session_token_created' => true, 'auth_module' => $authModule]); + $this->withTokens(['sessionid' => $this->createDavToken('user1')]); + $userSession->expects($this->once())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest(['HTTP_AUTHORIZATION' => 'Bearer neu'])); + } + + /** + * Anmeldung über ein Anmeldemodul: Die Sitzung merkt sich Modul und + * Fingerabdruck des Authorization-Kopfs, nicht den Kopf selbst. + */ + public function testTryAuthModuleLoginRemembersModuleAndCredential() { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user1'); + $user->method('isEnabled')->willReturn(true); + $module = $this->createMock(IAuthModule::class); + $module->method('auth')->willReturn($user); + $store = new Memory(); + /** @var Session | \PHPUnit\Framework\MockObject\MockObject $userSession */ + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$this->createMock(Manager::class), $store, $this->timeFactory, $this->tokenProvider, $this->config, $this->logger, $this->serviceLoader, $this->userSyncService, $this->eventDispatcher]) + ->onlyMethods(['getAuthModules', 'createSessionToken', 'loginUser']) + ->getMock(); + $userSession->method('getAuthModules')->willReturn([$module]); + $userSession->expects($this->once())->method('loginUser')->willReturn(true); + $userSession->expects($this->once())->method('createSessionToken'); + + $this->assertTrue($userSession->tryAuthModuleLogin($this->createDavRequest(['HTTP_AUTHORIZATION' => 'Bearer abc']))); + $this->assertSame('user1', $store->get('AUTHENTICATED_TO_DAV_BACKEND')); + $this->assertSame( + ['class' => \get_class($module), 'credential' => \hash('sha256', 'Bearer abc')], + $store->get('auth_module') + ); + } } From 53397d1f17df45396512f7373503988f615e2be1 Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Tue, 20 Oct 2026 23:13:47 +0200 Subject: [PATCH 06/11] =?UTF-8?q?fix(session):=20Erneuern=20beh=C3=A4lt=20?= =?UTF-8?q?die=20Sitzung,=20Kennwortwechsel=20beendet=20sie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nachbesserung zu cb5c6e3081. Desktop 6.0.3 schickt die Token-Erneuerung (POST /index.php/apps/oauth2/api/v1/token, Client-Daten über HTTP-Basic) über die Kontoverbindung samt Sitzungs-Cookie. validateDavSession() sah dort einen anderen Authorization-Kopf als den bestätigten Bearer, das OAuth2-Modul antwortete für Basic ohne Nutzer, und die Sitzung endete. Jede Erneuerung kostete so die Sitzung: Die nächsten 20 parallelen PROPFIND meldeten sich alle neu an. Ebenso endete sie mit einem abgelaufenen Access-Token zur fälligen Prüfung, mit dem alten Token, das nach der Erneuerung noch unterwegs war, am Token-Endpunkt mit Client-Daten im Formular und an der OpenID-Connect-Discovery (/apps/openidconnect/config, Ziel von /.well-known/openid-configuration). - Token-Endpunkt (POST) und Discovery (GET) lassen die Sitzung unberührt: kein Vermerk, keine Aktivität, gleich mit welchem Authorization-Kopf. Verglichen werden Pfad (getRawPathInfo()) und Methode wie in BasicAuthModule. - Lehnt das Anmeldemodul einen Bearer mit einer Ausnahme ab (abgelaufen oder widerrufen, für den Kern nicht zu unterscheiden), lässt die Prüfung die Sitzung für das erneuerte Token bestehen. Die Anfrage selbst weisen WebDAV (Bearer-Backend des Moduls) sowie OCS und Weboberfläche (verifyAuthHeaders(), beendet die Sitzung dabei wie bisher) ohnehin mit 401 ab. Ohne Bearer, mit dem Bearer eines anderen Nutzers, ohne Modul oder wenn das Modul ohne Nutzer antwortet, endet sie wie bisher: Ohne Nutzer bürgt das Modul nicht für den Kopf, und verifyAuthHeaders() ließe die Anfrage über das Sitzungstoken durch. Der Vorschlag der Prüfung, nur bei Ausnahme zu beenden und bei null nicht, hätte bei oauth2 0.10.0 nicht gegriffen: Für abgelaufene Tokens wirft dessen AuthModule (LoginException „Invalid token“). Ohne Nutzer antwortet es nur ohne Bearer und für unbekannte Tokens, wenn die App openidconnect eingeschaltet ist. Kennwortwechsel (zweiter Befund, bestand schon vorher): Client-Sitzungen ohne Anmeldemodul prüfen jetzt alle core/last_check_timeout Minuten das hinterlegte Kennwort, wie Browser-Sitzungen und wie eine Anmeldung mit denselben Zugangsdaten: - App-Passwort: das Kennwort, mit dem es angelegt wurde. Gilt es nicht mehr, wird das App-Passwort ungültig und die Sitzung endet, wie bisher bei einer Anmeldung damit; Konten aus LDAP ausgenommen, wie dort. - Kennwort-Sitzung nur mit Cookie: das bei der Anmeldung hinterlegte Kennwort. Mit Kennwort im Kopf prüft wie bisher BasicAuthModule. Von parallelen Anfragen mit demselben Token prüft nur eine: IProvider::claimCredentialCheck() schreibt last_check mit einem bedingten UPDATE (DefaultTokenMapper::claimLastCheck()). Lässt sich das Kennwort nicht prüfen (Token ohne Kennwort, Ausnahme der Benutzerverwaltung), bleibt die Sitzung bis zur nächsten Prüfung. Log ersetzt in Stacktraces die Parameter der neuen Methoden, die App-Passwort oder Sitzungskennung tragen. Changelog und troubleshooting.md beschreiben das Verhalten jetzt, wie es ist, auch die Bremse: Prüfungen mit veraltetem Kennwort zählen als Fehlversuch. Tests: SessionTest (Token-Endpunkt mit Basic und Formular, Discovery, GET am Token-Pfad, anderer Präfix, abgelehnter alter und fälliger Bearer, Modul ohne Nutzer, Fristgrenze, Kennwortprüfung für App-Passwort und Kennwort-Sitzung samt LDAP, Token ohne Kennwort, Ausfall der Benutzerverwaltung, parallele Übernahme), DefaultTokenProviderTest, DefaultTokenMapperTest (Datenbank) und LoggerTest. Ohne die Änderung scheitern die neuen Tests (SessionTest 18, Provider 3, Mapper 2, Logger 27), mit ihr sind alle grün; 20 Mutationen der Regeln, alle erkannt. Volle Suite: dieselben 44 umgebungsbedingten Fehler (Vorschau) wie zuvor, keine neuen. php-cs-fixer und phan ohne Befund, phpstan ohne Befund in den geänderten Dateien. Nachgestellt an einer eigenen Instanz (11.0.40 + Zweig, MariaDB, Redis mit Sitzungssperre wie testoo, oauth2 0.10.0), vorher → nachher: - Erneuerung mit Cookie (Basic, Formular), Discovery mit Cookie: Sitzung beendet → bleibt - abgelaufenes Token, Prüfung fällig: 401, Sitzung beendet → 401, bleibt; alter Bearer nach der Erneuerung: beendet, eine Vollanmeldung → bleibt; ungültiger Bearer + Cookie an WebDAV: 401, beendet → 401, bleibt (OCS 401 und beendet wie vorher) - Erneuerung, dann 20 parallel: je Runde 20 Vollanmeldungen, p50 0,50–0,53 s, max 0,99–1,08 s → keine, p50 87–104 ms, max 186–216 ms (dev-503-sitzung 64–85 ms, max 110–199 ms). 7–11 Antworten schicken dieselbe Sitzungskennung noch einmal (zweites session_start() beim Schreiben des neuen Fingerabdrucks), keine neue. - Kennwortwechsel, Prüfung fällig: Kennwort-Sitzung nur Cookie 207 → 401, App-Passwort + Cookie und nur Cookie 207 → 401, App-Passwort gelöscht - 20 parallel bei fälliger Prüfung nach Kennwortwechsel: eine Prüfung („Login failed“ 1, dazu 1 der abgewiesenen Neuanmeldung), 19 × 207, 1 × 401; mit gültigem Kennwort 20 × 207, p50 81 ms - unverändert: fremder Bearer, Widerruf von App-Passwort und Desktop-Freigabe (auch parallel), entfernte Sitzung, nur Cookie zu einer OAuth2-Sitzung 401, OCS, Gast, Browser; Bursts mit Bearer, App-Passwort und Kennwort wie dev-503-sitzung (p50 70–76 ms, p95 135–136 ms) - Playwright: Anmeldung, Weboberfläche, öffentlicher Link, Gast grün --- .../unreleased/revoke-ends-client-sessions | 38 ++- docs/administration/troubleshooting.md | 32 +- .../Token/DefaultTokenMapper.php | 19 ++ .../Token/DefaultTokenProvider.php | 15 + .../Authentication/Token/IProvider.php | 13 + lib/private/Log.php | 4 + lib/private/User/Session.php | 241 +++++++++++--- .../Token/DefaultTokenMapperTest.php | 19 ++ .../Token/DefaultTokenProviderTest.php | 33 +- tests/lib/LoggerTest.php | 46 +++ tests/lib/User/SessionTest.php | 296 ++++++++++++++++-- 11 files changed, 675 insertions(+), 81 deletions(-) diff --git a/changelog/unreleased/revoke-ends-client-sessions b/changelog/unreleased/revoke-ends-client-sessions index b4f48d2a42..05139550a3 100644 --- a/changelog/unreleased/revoke-ends-client-sessions +++ b/changelog/unreleased/revoke-ends-client-sessions @@ -9,19 +9,35 @@ they were started with, and every request extended them. Removing such a session under "Sessions" in the settings had no effect either, its session token was simply created again with the next request. A session started with an OAuth2 token went on accepting requests with the cookie alone after the -desktop authorization was revoked. +desktop authorization was revoked, and a password change did not end any of +these sessions. These sessions are now checked before every request, like browser sessions: - App password: the session ends with the next request after the app password - was revoked, also when the client only sends the cookie. + was revoked, also when the client only sends the cookie. Every + core/last_check_timeout minutes (default 5) the password the app password + was created with is checked, as a login with the app password does; after a + password change the app password becomes invalid and the session ends. + Accounts from LDAP are exempt, as on login. +- Password: when the client sends only the cookie, the password stored with + the session is checked every core/last_check_timeout minutes; after a + password change the session ends. Requests that send the password are + checked by the HTTP Basic login as before. - Session removed in the settings: the session ends with the next request. A client with valid credentials signs in again and gets a new session. -- OAuth2 and OpenID Connect: every request has to carry the Authorization - header that the authentication module confirmed. A different header, or none - at all, is checked by the module at once, the same header again after - core/last_check_timeout minutes (default 5). As before, the module itself - rejects WebDAV requests with a revoked token right away. +- OAuth2 and OpenID Connect: every request has to carry an Authorization + header that the authentication module confirms for the user of the session. + A different header, or none at all, is checked by the module at once, the + same header again after core/last_check_timeout minutes. Without a token or + with the token of another user the session ends. If the module rejects the + token, for example because it has expired or was revoked, the request fails + as before (401); on WebDAV the session stays for the renewed token, OCS and + the web interface end it as before. After a revocation the session can no + longer be used. +- The token endpoint of the OAuth2 app and the OpenID Connect discovery, which + the desktop client calls with its cookie but without its token, leave the + session alone. Renewing a token no longer ends the session. A session that is no longer valid is logged out and its session token is deleted; the request continues without a login and gets 401 unless it carries @@ -34,6 +50,8 @@ session was not started with an app password, the client signs in once more. The check reads the session token from the database as before, for app passwords also the app password token, which the HTTP Basic login of the same request loads anyway. The session and the token are written only when -something changes, the last activity of the token at most once a minute. -Browser sessions, public links, guests and the early release of the session -lock for sync clients are unchanged. +something changes, the last activity of the token at most once a minute. Of +parallel requests with the same token only one checks a stored password; it +takes over the check with a single conditional update. Browser sessions, public +links, guests and the early release of the session lock for sync clients are +unchanged. diff --git a/docs/administration/troubleshooting.md b/docs/administration/troubleshooting.md index c0a3a3018e..0ff3054e1d 100644 --- a/docs/administration/troubleshooting.md +++ b/docs/administration/troubleshooting.md @@ -110,15 +110,34 @@ vor jeder Anfrage: - **App-Passwort entzogen** (Persönlich → Sicherheit → App-Passwörter / Token): Die nächste Anfrage des Geräts bekommt 401, auch wenn es nur das Cookie schickt. Der Client muss mit einem neuen App-Passwort eingerichtet werden. +- **Kennwort geändert**: Sitzungen, die mit dem Kennwort begannen, enden + spätestens `last_check_timeout` Minuten (App-Wert `core`, Vorgabe 5) nach der + Änderung, auch wenn das Gerät nur das Cookie schickt. App-Passwörter, die vor + der Änderung entstanden, werden mit ihrer nächsten Prüfung ungültig (wie + bisher bei einer Anmeldung damit), ihre Sitzungen enden; der Client braucht + ein neues App-Passwort. Ausgenommen sind Konten aus LDAP: Deren App-Passwörter + prüft der Server nicht gegen das Kennwort, sie müssen entzogen werden. Jede + dieser Prüfungen mit dem alten Kennwort steht als „Login failed“ im Protokoll + und zählt wie ein Fehlversuch für die Bremse gegen Passwort-Raten. - **Sitzung unter „Sitzungen“ entfernt**: Die Sitzung endet mit der nächsten Anfrage. Hat das Gerät noch gültige Zugangsdaten (App-Passwort, OAuth2-Token, Kennwort), meldet es sich damit sofort neu an. Um ein Gerät auszusperren, - dessen Zugang entziehen, nicht nur die Sitzung. + dessen Zugang entziehen oder das Kennwort ändern, nicht nur die Sitzung + entfernen. - **Desktop-Freigabe (OAuth2) widerrufen**: WebDAV- und OCS-Anfragen mit dem - Token werden sofort abgewiesen. Die Sitzung selbst endet mit der ersten - Anfrage ohne das Token (etwa nur mit Cookie), sonst mit der ersten Anfrage, - die mehr als `last_check_timeout` Minuten (App-Wert `core`, Vorgabe 5) nach - der letzten Prüfung des Tokens kommt. + Token werden sofort abgewiesen. Die Sitzung lässt sich danach nicht mehr + nutzen: Sie gilt nur zusammen mit einem Token, das die OAuth2-App für ihr + Konto bestätigt, und endet mit der ersten Anfrage ohne Token (etwa nur mit + Cookie), spätestens mit ihrem Ablauf (`session_lifetime`). + +Ein abgelaufenes Access-Token beendet die Sitzung bei WebDAV nicht: Die Anfrage +damit bekommt 401, der Client erneuert das Token und arbeitet mit derselben +Sitzung weiter. Kommt es zuerst über OCS oder die Weboberfläche, endet die +Sitzung dort wie bisher, und der Client meldet sich mit dem neuen Token neu an. +Token-Erneuerung (`POST /index.php/apps/oauth2/api/v1/token`) und +OpenID-Connect-Discovery (`/index.php/apps/openidconnect/config`, Ziel von +`/.well-known/openid-configuration`) ruft der Desktop-Client mit dem Cookie, aber +ohne Token auf; sie lassen die Sitzung unberührt. Eine so beendete Sitzung steht mit Stufe info im Protokoll: „Client session of `` ended: ``“. Anfragen, die zu einer per @@ -163,4 +182,5 @@ Prüfen: Die Token-Anfrage (`POST /index.php/apps/oauth2/api/v1/token`) weist mit HTTP-Basic den Client aus, nicht den Benutzer. Sie zählt deshalb nicht als Anmeldeversuch: kein „Login failed“ im Protokoll, keine Bremse gegen -Passwort-Raten, und eine mitgeschickte Sitzung bleibt bestehen. +Passwort-Raten, und eine mitgeschickte Sitzung bleibt bestehen – auch wenn der +Client seine Daten im Formular statt über HTTP-Basic schickt. diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php index c90464bb73..2b7e0d9a1e 100644 --- a/lib/private/Authentication/Token/DefaultTokenMapper.php +++ b/lib/private/Authentication/Token/DefaultTokenMapper.php @@ -64,6 +64,25 @@ public function invalidateOld($olderThan) { ->execute(); } + /** + * Setzt last_check eines Tokens auf $now, wenn es noch $lastCheck ist – + * Vergleichen und Setzen in einer Anweisung, damit von parallelen Anfragen + * genau eine die fällige Prüfung übernimmt. + * + * @param int $id + * @param int $lastCheck der beim Laden gelesene Wert + * @param int $now + * @return bool true, wenn dieser Aufruf den Wert gesetzt hat + */ + public function claimLastCheck($id, $lastCheck, $now) { + $qb = $this->db->getQueryBuilder(); + $qb->update('authtoken') + ->set('last_check', $qb->createNamedParameter($now, IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('last_check', $qb->createNamedParameter($lastCheck, IQueryBuilder::PARAM_INT))); + return (int)$qb->execute() === 1; + } + /** * Get the user UID for the given token * diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php index 3dbf084f66..448d7ed9a4 100644 --- a/lib/private/Authentication/Token/DefaultTokenProvider.php +++ b/lib/private/Authentication/Token/DefaultTokenProvider.php @@ -158,6 +158,21 @@ public function updateTokenActivity(IToken $token) { } } + /** + * @inheritdoc + */ + public function claimCredentialCheck(IToken $token, $now) { + if (!($token instanceof DefaultToken)) { + throw new InvalidTokenException(); + } + $claimed = $this->mapper->claimLastCheck($token->getId(), (int)$token->getLastCheck(), $now); + $this->tokenMemo = []; + if ($claimed) { + $token->setLastCheck($now); + } + return $claimed; + } + /** * Get all token of a user * diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index 9b20562dc8..b88559325d 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -77,6 +77,19 @@ public function updateToken(IToken $token); */ public function updateTokenActivity(IToken $token); + /** + * Übernimmt die fällige Prüfung der Zugangsdaten eines Tokens: setzt seinen + * Zeitpunkt der letzten Prüfung auf $now, aber nur, wenn ihn seit dem Laden + * niemand sonst gesetzt hat. Von parallelen Anfragen mit demselben Token prüft + * so genau eine. + * + * @param IToken $token + * @param int $now + * @throws InvalidTokenException + * @return bool true, wenn dieser Aufruf die Prüfung übernimmt + */ + public function claimCredentialCheck(IToken $token, $now); + /** * Get all token of a user * diff --git a/lib/private/Log.php b/lib/private/Log.php index a7cce74588..f120568052 100644 --- a/lib/private/Log.php +++ b/lib/private/Log.php @@ -87,6 +87,10 @@ class Log implements ILogger { 'loginWithPassword', 'createSessionToken', 'loginInOwnCloud', + // Prüfung von Client-Sitzungen: App-Passwort und Sitzungskennung als Parameter + 'checkClientCredential', + 'checkAppPassword', + 'isStoredPasswordValid', // TokenProvider 'getToken', diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index 2ba4681cda..d5cf38a9be 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -109,6 +109,20 @@ class Session implements IUserSession, Emitter { */ private const AUTH_MODULE = 'auth_module'; + /** + * Endpunkte, die Clients mit dem Sitzungs-Cookie ihrer Kontoverbindung, aber + * ohne den Bearer der Sitzung aufrufen (Pfad wie getRawPathInfo() => Methode): + * der Token-Endpunkt der App oauth2 (Client-Daten über HTTP-Basic oder im + * Formular, bei jeder Token-Erneuerung) und die OpenID-Connect-Discovery der App + * openidconnect (Ziel von /.well-known/openid-configuration, vor jeder + * Erneuerung). Beide brauchen die Sitzung nicht; validateDavSession() lässt sie + * dort unberührt, statt sie mangels Bearer zu beenden. + */ + private const CLIENT_ENDPOINTS = [ + '/apps/oauth2/api/v1/token' => 'POST', + '/apps/openidconnect/config' => 'GET', + ]; + /** @var IUserManager | PublicEmitter $manager */ private $manager; @@ -303,18 +317,31 @@ public function validateSession() { * * Eine solche Sitzung gilt nur, solange gilt, womit sie angemeldet wurde: * - App-Passwort: Es muss noch bestehen. Wird es widerrufen, endet die Sitzung - * mit der nächsten Anfrage, auch wenn der Client nur das Cookie schickt. + * mit der nächsten Anfrage, auch wenn der Client nur das Cookie schickt. Das + * Kennwort, mit dem es angelegt wurde, prüft der Server alle + * core/last_check_timeout Minuten wie bei einer Anmeldung mit dem App-Passwort + * (Konten aus LDAP ausgenommen, wie dort); gilt es nicht mehr, wird das + * App-Passwort ungültig und die Sitzung endet. + * - Kennwort: Schickt der Client nur das Cookie, prüft der Server das bei der + * Anmeldung hinterlegte Kennwort alle core/last_check_timeout Minuten, wie für + * Browser; schickt er es mit, prüft es BasicAuthModule wie bisher. * - Sitzungstoken: Hatte die Sitzung schon eines, muss es noch bestehen (etwa * unter „Sitzungen“ entfernt). Fehlt es noch, legt diese Anfrage es an – wie * bisher erst, wenn der Client das Cookie zurückschickt, damit Clients ohne * Cookies nicht mit jeder Anfrage ein Token erzeugen. - * - Anmeldemodul (Bearer): Jede Anfrage muss den Authorization-Kopf tragen, - * den das Modul zuletzt bestätigt hat. Einen anderen – auch gar keinen – - * prüft das Modul sofort, denselben spätestens nach core/last_check_timeout - * Minuten wieder (wie die Kennwortprüfung). Für WebDAV prüft das Modul den - * Bearer ohnehin bei jeder Anfrage. Kommt ein solcher Kopf zu einer Sitzung - * ohne diesen Vermerk (begonnen vor dieser Prüfung), muss ein Modul ihren - * Nutzer bestätigen und wird dann vermerkt. + * - Anmeldemodul (Bearer): Jede Anfrage muss einen Authorization-Kopf tragen, + * den das Modul für den Nutzer der Sitzung bestätigt. Einen anderen als den + * zuletzt bestätigten – auch gar keinen – prüft das Modul sofort, denselben + * spätestens nach core/last_check_timeout Minuten wieder. Lehnt es einen + * Bearer ab (abgelaufenes oder widerrufenes Token), lässt diese Prüfung die + * Sitzung für das erneuerte Token bestehen; die Anfrage selbst weisen + * WebDAV, OCS und Weboberfläche ab (die beiden letzten beenden die Sitzung + * dabei wie bisher). Ohne Bearer, mit einem, für den das Modul ohne Nutzer + * antwortet, oder mit dem eines anderen Nutzers endet sie. Kommt ein Bearer + * zu einer Sitzung ohne diesen Vermerk (begonnen vor dieser Prüfung), muss + * ein Modul ihren Nutzer bestätigen und wird dann vermerkt. + * - Token-Endpunkt und Discovery (self::CLIENT_ENDPOINTS) ruft der Client mit + * dem Cookie, aber ohne Bearer auf; sie lassen die Sitzung unberührt. * - Eine Sitzung von vor dieser Prüfung ohne App-Passwort, deren erste Anfrage * danach nur das Cookie schickt, meldet sich einmal neu an: Ob sie mit * Kennwort oder Bearer begann, ist nicht bekannt. @@ -339,8 +366,16 @@ public function validateDavSession(IRequest $request) { } $appPassword = $this->session->get('app_password'); - if ($appPassword !== null && !$this->isTokenOfUser($appPassword, $sessionUser)) { - $this->endDavSession($sessionUser, 'app password no longer valid'); + if ($appPassword !== null) { + $problem = $this->checkAppPassword($appPassword, $sessionUser); + if ($problem !== null) { + $this->endDavSession($sessionUser, $problem); + return; + } + } + + if (self::isClientEndpoint($request)) { + // Weder Vermerk noch Aktivität: Die Anfrage nutzt die Sitzung nicht. return; } @@ -370,9 +405,14 @@ public function validateDavSession(IRequest $request) { $beganBeforeCheck = true; } - $problem = $this->checkClientCredential($sessionUser, $request, $sessionToken, $appPassword !== null, $beganBeforeCheck); - if ($problem !== null) { - $this->endDavSession($sessionUser, $problem); + $verdict = $this->checkClientCredential($sessionUser, $request, $sessionId, $sessionToken, $appPassword !== null, $beganBeforeCheck); + if ($verdict === false) { + // Weder bestätigt noch widerlegt (siehe checkAuthModuleCredential()): + // Sitzung unverändert lassen, auch ihre Aktivität. + return; + } + if ($verdict !== null) { + $this->endDavSession($sessionUser, $verdict); return; } @@ -387,16 +427,18 @@ public function validateDavSession(IRequest $request) { * * @param IUser $sessionUser * @param IRequest $request - * @param IToken|null $sessionToken + * @param string $sessionId + * @param IToken|null $sessionToken null, wenn diese Anfrage es eben erst angelegt hat * @param bool $hasAppPassword die Sitzung begann mit einem App-Passwort * @param bool $beganBeforeCheck die Sitzung begann vor dieser Prüfung - * @return string|null warum die Sitzung nicht mehr gilt, null wenn sie gilt + * @return string|false|null warum die Sitzung nicht mehr gilt; null, wenn sie + * gilt; false, wenn die Anfrage sie weder bestätigt + * noch widerlegt */ - private function checkClientCredential(IUser $sessionUser, IRequest $request, ?IToken $sessionToken, $hasAppPassword, $beganBeforeCheck) { + private function checkClientCredential(IUser $sessionUser, IRequest $request, $sessionId, ?IToken $sessionToken, $hasAppPassword, $beganBeforeCheck) { $authModule = $this->session->get(self::AUTH_MODULE); if ($authModule !== null) { - return $this->isAuthModuleCredentialValid($authModule, $sessionUser, $request, $sessionToken) - ? null : 'credential not confirmed by the authentication module'; + return $this->checkAuthModuleCredential($authModule, $sessionUser, $request, $sessionToken); } if ($hasAppPassword) { return null; @@ -405,26 +447,96 @@ private function checkClientCredential(IUser $sessionUser, IRequest $request, ?I return $this->adoptAuthModule($sessionUser, $request) ? null : 'credential not confirmed by any authentication module'; } - if ($beganBeforeCheck && self::getAuthorizationHeader($request) === '' && !isset($request->server['PHP_AUTH_USER'])) { + if (!self::sendsNoCredentials($request)) { + // Kennwort über HTTP-Basic: prüft BasicAuthModule alle paar Minuten + return null; + } + if ($beganBeforeCheck) { // Ob eine Sitzung von vor dieser Prüfung mit Kennwort oder Bearer // begann, ist unbekannt; nur mit Cookie ließe sich ein widerrufener // Bearer nie erkennen. Einmal neu anmelden. return 'session from before the check without credentials'; } + if ($sessionToken !== null && !$this->isStoredPasswordValid($sessionToken, $sessionId)) { + return 'password changed'; + } return null; } /** - * @param string $token - * @param IUser $user - * @return bool true, wenn das Token besteht und dem Nutzer gehört + * Gilt das App-Passwort der Sitzung noch? Es muss bestehen und dem Nutzer + * gehören. Das Kennwort, mit dem es angelegt wurde, prüft der Server wie bei + * einer Anmeldung mit dem App-Passwort (validateToken()): alle + * core/last_check_timeout Minuten und nicht bei Konten aus LDAP, deren + * App-Passwörter sonst schon ein kurzer Ausfall des Verzeichnisses löschte. + * Gilt das Kennwort nicht mehr, wird das App-Passwort ungültig, wie dort. + * + * @param string $appPassword + * @param IUser $sessionUser + * @return string|null warum die Sitzung nicht mehr gilt, null wenn sie gilt + */ + private function checkAppPassword($appPassword, IUser $sessionUser) { + try { + $token = $this->tokenProvider->getToken($appPassword); + } catch (InvalidTokenException $ex) { + $token = null; + } + if ($token === null || $token->getUID() !== $sessionUser->getUID()) { + return 'app password no longer valid'; + } + if ($sessionUser->getBackendClassName() !== 'LDAP' && !$this->isStoredPasswordValid($token, $appPassword)) { + $this->tokenProvider->invalidateToken($appPassword); + return 'password of the app password changed'; + } + return null; + } + + /** + * Gilt das Kennwort noch, das im Token hinterlegt ist? Geprüft wird höchstens + * alle core/last_check_timeout Minuten (last_check am Token, wie in + * checkTokenCredentials()), und von parallelen Anfragen mit demselben Token nur + * von der, die die Prüfung übernimmt; sonst prüften sie alle zugleich, je mit + * einem Kennwort-Hash oder einer Anmeldung am Verzeichnis. Lässt sich das + * Kennwort gerade nicht prüfen (Token ohne Kennwort, Ausnahme der + * Benutzerverwaltung), gilt es bis zur nächsten Prüfung weiter. + * + * @param IToken $dbToken + * @param string $tokenId + * @return bool */ - private function isTokenOfUser($token, IUser $user) { + private function isStoredPasswordValid(IToken $dbToken, $tokenId) { + $now = $this->timeFactory->getTime(); + if (!$this->isCredentialCheckDue($dbToken, $now) + || !$this->tokenProvider->claimCredentialCheck($dbToken, $now) + ) { + return true; + } try { - return $this->tokenProvider->getToken($token)->getUID() === $user->getUID(); + $password = $this->tokenProvider->getPassword($dbToken, $tokenId); + } catch (PasswordlessTokenException $ex) { + return true; } catch (InvalidTokenException $ex) { return false; } + try { + return $this->manager->checkPassword($dbToken->getLoginName(), $password) !== false; + } catch (Exception $ex) { + $this->logger->logException($ex, ['app' => __METHOD__]); + return true; + } + } + + /** + * Ist die nächste Prüfung der Zugangsdaten fällig, die das Token vertritt? + * Abstand: App-Wert core/last_check_timeout in Minuten (Vorgabe 5). + * + * @param IToken $token + * @param int $now + * @return bool + */ + private function isCredentialCheckDue(IToken $token, $now) { + $checkTimeout = 60 * (int)$this->config->getAppValue('core', 'last_check_timeout', 5); + return $token->getLastCheck() <= $now - $checkTimeout; } /** @@ -434,37 +546,54 @@ private function isTokenOfUser($token, IUser $user) { * core/last_check_timeout Minuten her ist; vermerkt wird sie wie die * Kennwortprüfung am Sitzungstoken. * + * Lehnt das Modul einen Bearer mit einer Ausnahme ab, bleibt die Sitzung + * unverändert: Abgelaufene und widerrufene Tokens sehen für den Kern gleich + * aus, und mit einem abgelaufenen holt sich der Client gleich ein neues. Die + * Anfrage selbst weisen WebDAV (Bearer-Backend des Moduls) sowie OCS und + * Weboberfläche (verifyAuthHeaders(), beendet die Sitzung dabei wie bisher) + * ohnehin ab. Antwortet das Modul ohne Nutzer, bürgt es nicht für den Kopf, + * und verifyAuthHeaders() ließe die Anfrage über die Sitzung durch: Dann + * endet sie. + * * @param mixed $authModule Wert von self::AUTH_MODULE * @param IUser $sessionUser * @param IRequest $request * @param IToken|null $sessionToken null, wenn diese Anfrage es eben erst angelegt hat - * @return bool + * @return string|false|null wie checkClientCredential() */ - private function isAuthModuleCredentialValid($authModule, IUser $sessionUser, IRequest $request, ?IToken $sessionToken) { + private function checkAuthModuleCredential($authModule, IUser $sessionUser, IRequest $request, ?IToken $sessionToken) { $class = \is_array($authModule) ? ($authModule['class'] ?? null) : null; $confirmed = \is_array($authModule) ? ($authModule['credential'] ?? null) : null; $credential = self::getCredentialFingerprint($request); $changed = !\is_string($confirmed) || !\hash_equals($confirmed, $credential); $now = $this->timeFactory->getTime(); - $checkTimeout = 60 * (int)$this->config->getAppValue('core', 'last_check_timeout', 5); - if (!$changed && ($sessionToken === null || $sessionToken->getLastCheck() > $now - $checkTimeout)) { - return true; + if (!$changed && ($sessionToken === null || !$this->isCredentialCheckDue($sessionToken, $now))) { + return null; } - $user = null; - foreach ($this->getAuthModules(false) as $module) { - if (\get_class($module) !== $class) { - continue; + $module = null; + foreach ($this->getAuthModules(false) as $candidate) { + if (\get_class($candidate) === $class) { + $module = $candidate; + break; } - try { - $user = $module->auth($request); - } catch (Exception $ex) { - $user = null; + } + if ($module === null) { + return 'authentication module not available'; + } + try { + $user = $module->auth($request); + } catch (Exception $ex) { + if (self::sendsModuleCredential($request)) { + return false; } - break; + $user = null; } - if ($user === null || $user->getUID() !== $sessionUser->getUID()) { - return false; + if ($user === null) { + return 'credential not confirmed by the authentication module'; + } + if ($user->getUID() !== $sessionUser->getUID()) { + return 'credential of another user'; } if ($changed) { @@ -474,7 +603,7 @@ private function isAuthModuleCredentialValid($authModule, IUser $sessionUser, IR $sessionToken->setLastCheck($now); $this->tokenProvider->updateToken($sessionToken); } - return true; + return null; } /** @@ -532,6 +661,36 @@ private static function sendsModuleCredential(IRequest $request) { return $authorization !== '' && \stripos($authorization, 'Basic ') !== 0; } + /** + * Schickt die Anfrage gar keine Zugangsdaten, nur das Sitzungs-Cookie? Basic + * steht unter mod_php auch ohne Authorization-Kopf in PHP_AUTH_USER. + * + * @param IRequest $request + * @return bool + */ + private static function sendsNoCredentials(IRequest $request) { + return self::getAuthorizationHeader($request) === '' && !isset($request->server['PHP_AUTH_USER']); + } + + /** + * Ruft die Anfrage einen der self::CLIENT_ENDPOINTS auf? Verglichen wird mit + * getRawPathInfo() – demselben Pfad, nach dem der Router zuteilt – und der + * Methode der Route, wie in BasicAuthModule. + * + * @param IRequest $request + * @return bool + */ + private static function isClientEndpoint(IRequest $request) { + try { + $path = $request->getRawPathInfo(); + } catch (\Exception $e) { + // Pfad nicht bestimmbar: dann teilt auch der Router die Anfrage keinem + // dieser Endpunkte zu. + return false; + } + return isset(self::CLIENT_ENDPOINTS[$path]) && self::CLIENT_ENDPOINTS[$path] === $request->getMethod(); + } + /** * @param IRequest $request * @return string diff --git a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php index 11ce24b7be..f34325a40d 100644 --- a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php @@ -146,6 +146,25 @@ public function testGetToken() { $this->assertEquals($token, $dbToken); } + /** + * Von parallelen Anfragen mit demselben Token übernimmt genau eine die fällige + * Prüfung: Nur wer last_check noch mit dem geladenen Wert antrifft, setzt ihn. + */ + public function testClaimLastCheckSucceedsOnlyOnce() { + $dbToken = $this->mapper->getToken('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'); + $loaded = (int)$dbToken->getLastCheck(); + + $this->assertTrue($this->mapper->claimLastCheck($dbToken->getId(), $loaded, $this->time)); + $this->assertFalse($this->mapper->claimLastCheck($dbToken->getId(), $loaded, $this->time + 1)); + + $this->assertSame($this->time, (int)$this->mapper->getToken($dbToken->getToken())->getLastCheck()); + } + + public function testClaimLastCheckOfMissingToken() { + $this->assertFalse($this->mapper->claimLastCheck(-1, $this->time - 600, $this->time)); + $this->assertSame(3, $this->getNumberOfTokens()); + } + /** */ public function testGetInvalidToken() { diff --git a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php index bb5695271f..41f5a9be40 100644 --- a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php @@ -120,7 +120,38 @@ public function testUpdateTokenDebounce() { $this->tokenProvider->updateTokenActivity($tk); } - + + public function providesCredentialCheckClaims() { + return [ + 'übernommen' => [true], + 'eine parallele Anfrage war schneller' => [false], + ]; + } + + /** + * @dataProvider providesCredentialCheckClaims + * @param bool $claimed + */ + public function testClaimCredentialCheck($claimed) { + $tk = new DefaultToken(); + $tk->setId(42); + $tk->setLastCheck($this->time - 600); + $this->mapper->expects($this->once()) + ->method('claimLastCheck') + ->with(42, $this->time - 600, $this->time) + ->willReturn($claimed); + + $this->assertSame($claimed, $this->tokenProvider->claimCredentialCheck($tk, $this->time)); + $this->assertSame($claimed ? $this->time : $this->time - 600, $tk->getLastCheck()); + } + + public function testClaimCredentialCheckOfForeignToken() { + $this->expectException(\OC\Authentication\Exceptions\InvalidTokenException::class); + $this->mapper->expects($this->never())->method('claimLastCheck'); + + $this->tokenProvider->claimCredentialCheck($this->createMock(\OC\Authentication\Token\IToken::class), $this->time); + } + public function testGetTokenByUser() { $user = $this->createMock('\OCP\IUser'); $this->mapper->expects($this->once()) diff --git a/tests/lib/LoggerTest.php b/tests/lib/LoggerTest.php index 4931ac7c52..fd0799d644 100644 --- a/tests/lib/LoggerTest.php +++ b/tests/lib/LoggerTest.php @@ -330,6 +330,52 @@ public function testDetectcreateSessionToken($request, $uid, $loginName, $passwo } } + /** + * Session::checkAppPassword() bekommt das App-Passwort als Parameter. + * + * @dataProvider userAndPasswordData + */ + public function testDetectcheckAppPassword($user, $password): void { + $this->assertSensitiveParametersReplaced('checkAppPassword', [$user, $password]); + } + + /** + * Session::isStoredPasswordValid() bekommt App-Passwort oder Sitzungskennung als Parameter. + * + * @dataProvider userAndPasswordData + */ + public function testDetectisStoredPasswordValid($user, $password): void { + $this->assertSensitiveParametersReplaced('isStoredPasswordValid', [$user, $password]); + } + + /** + * Session::checkClientCredential() bekommt die Sitzungskennung als Parameter. + * + * @dataProvider userAndPasswordData + */ + public function testDetectcheckClientCredential($user, $password): void { + $this->assertSensitiveParametersReplaced('checkClientCredential', [$user, $password]); + } + + /** + * Protokolliert eine hier ausgelöste Ausnahme (ihr Stack nennt die aufrufende + * Testmethode samt Parametern) und prüft, dass deren Werte ersetzt sind. + * + * @param string $method + * @param string[] $secrets + */ + private function assertSensitiveParametersReplaced(string $method, array $secrets): void { + $e = new \Exception('test'); + $this->logger->logException($e); + + foreach ($this->getLogs() as $logLine) { + foreach ($secrets as $secret) { + $this->assertStringNotContainsString($secret, $logLine['message']); + } + $this->assertStringContainsString("$method(*** sensitive parameters replaced ***)", $logLine['message']); + } + } + public function testPasswordInCallback(): void { $config = $this->createMock(IConfig::class); $logger = $this->createMock(ILogger::class); diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php index 55f946fac8..8acd50c461 100644 --- a/tests/lib/User/SessionTest.php +++ b/tests/lib/User/SessionTest.php @@ -2085,9 +2085,10 @@ public function testFailedLoginUserDisabled() { * @param array $values zusätzliche Sitzungswerte * @param IAuthModule[] $modules * @param bool $enabled - * @return array [Session (Mock), Sitzungsspeicher] + * @param string $backend Benutzerverwaltung des Nutzers + * @return array [Session (Mock), Sitzungsspeicher, Nutzerverwaltung (Mock)] */ - private function createDavSession(array $values, array $modules = [], $enabled = true) { + private function createDavSession(array $values, array $modules = [], $enabled = true, $backend = 'Database') { $store = new class extends Memory { public function getId() { return 'sessionid'; @@ -2104,6 +2105,7 @@ public function getId() { $user = $this->createMock(IUser::class); $user->method('getUID')->willReturn('user1'); $user->method('isEnabled')->willReturn($enabled); + $user->method('getBackendClassName')->willReturn($backend); $manager = $this->createMock(Manager::class); $manager->method('get')->willReturnCallback(function ($uid) use ($user) { return $uid === 'user1' ? $user : null; @@ -2116,7 +2118,7 @@ public function getId() { ->onlyMethods(['logout', 'getAuthModules']) ->getMock(); $userSession->method('getAuthModules')->willReturn($modules); - return [$userSession, $store]; + return [$userSession, $store, $manager]; } /** @@ -2127,18 +2129,29 @@ public function getId() { private function createDavToken($uid, $lastCheck = 10000) { $token = new DefaultToken(); $token->setUid($uid); + $token->setLoginName(\ucfirst($uid)); $token->setLastCheck($lastCheck); $token->setLastActivity(10000); return $token; } /** + * Ohne Angabe ein PROPFIND an WebDAV. + * * @param array $server + * @param string $method * @return Request */ - private function createDavRequest(array $server = []) { + private function createDavRequest(array $server = [], $method = 'PROPFIND') { return new Request( - ['server' => $server + ['HTTP_USER_AGENT' => 'mirall']], + [ + 'server' => $server + [ + 'HTTP_USER_AGENT' => 'mirall', + 'REQUEST_URI' => '/remote.php/dav/files/user1/', + 'SCRIPT_NAME' => '/remote.php', + ], + 'method' => $method, + ], $this->createMock(ISecureRandom::class), $this->createMock(IConfig::class), $this->createMock(CsrfTokenManager::class) @@ -2196,10 +2209,203 @@ public function testValidateDavSessionEndsWhenAppPasswordIsInvalid(array $owners return $context['reason'] === 'app password no longer valid'; })); $userSession->expects($this->once())->method('logout'); + // Nach dem Ende nichts mehr an der Sitzung: weder Token noch Aktivität + $this->tokenProvider->expects($this->never())->method('generateToken'); + $this->tokenProvider->expects($this->never())->method('updateTokenActivity'); $userSession->validateDavSession($this->createDavRequest()); } + public function providesStoredPasswordChecks() { + return [ + 'Prüfung noch nicht fällig' => [10000 - 299, false, true, false], + 'Prüfung genau fällig, Kennwort gilt' => [10000 - 300, true, true, false], + 'Prüfung fällig, Kennwort geändert' => [10000 - 301, true, false, true], + ]; + } + + /** + * Erwartet die Prüfung des hinterlegten Kennworts: erst übernommen (last_check + * atomar fortgeschrieben), dann abgeglichen, damit parallele Anfragen mit + * demselben Token sie nicht wiederholen. + * + * @param Manager | \PHPUnit\Framework\MockObject\MockObject $manager + * @param DefaultToken $token + * @param string $tokenId + * @param bool $checks + * @param bool $passwordValid + * @return \ArrayObject Reihenfolge der Schritte + */ + private function expectStoredPasswordCheck($manager, DefaultToken $token, $tokenId, $checks, $passwordValid) { + $steps = new \ArrayObject(); + $this->tokenProvider->method('getPassword')->willReturnCallback(function ($dbToken, $id) use ($token, $tokenId) { + $this->assertSame($token, $dbToken); + $this->assertSame($tokenId, $id); + return 'altes-kennwort'; + }); + $this->tokenProvider->expects($checks ? $this->once() : $this->never())->method('claimCredentialCheck') + ->with($token, 10000) + ->willReturnCallback(function (DefaultToken $dbToken, $now) use ($steps) { + $steps[] = 'vermerkt'; + $dbToken->setLastCheck($now); + return true; + }); + $manager->expects($checks ? $this->once() : $this->never())->method('checkPassword') + ->with('User1', 'altes-kennwort') + ->willReturnCallback(function () use ($steps, $passwordValid) { + $steps[] = 'geprüft'; + return $passwordValid ? $this->createMock(IUser::class) : false; + }); + return $steps; + } + + /** + * App-Passwort-Sitzung: Das Kennwort, mit dem das App-Passwort angelegt wurde, + * prüft der Server wie bei einer neuen Anmeldung damit alle last_check_timeout + * Minuten. Gilt es nicht mehr, wird das App-Passwort ungültig und die Sitzung + * endet – auch wenn der Client nur das Cookie schickt. + * + * @dataProvider providesStoredPasswordChecks + * @param int $lastCheck + * @param bool $checks + * @param bool $passwordValid + * @param bool $ends + */ + public function testValidateDavSessionChecksPasswordOfAppPassword($lastCheck, $checks, $passwordValid, $ends) { + [$userSession, , $manager] = $this->createDavSession(['app_password' => 'APPPW', 'session_token_created' => true]); + $appPasswordToken = $this->createDavToken('user1', $lastCheck); + $this->withTokens(['APPPW' => $appPasswordToken, 'sessionid' => $this->createDavToken('user1')]); + $steps = $this->expectStoredPasswordCheck($manager, $appPasswordToken, 'APPPW', $checks, $passwordValid); + $this->tokenProvider->expects($ends ? $this->once() : $this->never())->method('invalidateToken')->with('APPPW'); + $userSession->expects($ends ? $this->once() : $this->never())->method('logout'); + if ($ends) { + $this->logger->expects($this->once())->method('info') + ->with($this->stringContains('ended'), $this->callback(function ($context) { + return $context['reason'] === 'password of the app password changed'; + })); + } + + $userSession->validateDavSession($this->createDavRequest()); + + $this->assertSame($checks ? ['vermerkt', 'geprüft'] : [], $steps->getArrayCopy()); + $this->assertSame($checks ? 10000 : $lastCheck, $appPasswordToken->getLastCheck()); + } + + /** + * App-Passwörter von Konten aus LDAP prüft auch die Anmeldung mit dem + * App-Passwort nicht gegen das Kennwort (sonst löschte schon ein kurzer Ausfall + * des Verzeichnisses die App-Passwörter): hier ebenso. + */ + public function testValidateDavSessionKeepsAppPasswordOfLdapAccount() { + [$userSession, , $manager] = $this->createDavSession(['app_password' => 'APPPW', 'session_token_created' => true], [], true, 'LDAP'); + $this->withTokens(['APPPW' => $this->createDavToken('user1', 10000 - 301), 'sessionid' => $this->createDavToken('user1')]); + $this->tokenProvider->expects($this->never())->method('claimCredentialCheck'); + $manager->expects($this->never())->method('checkPassword'); + $this->tokenProvider->expects($this->never())->method('getPassword'); + $this->tokenProvider->expects($this->never())->method('invalidateToken'); + $userSession->expects($this->never())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest()); + } + + public function providesUncheckableStoredPasswords() { + return [ + 'Token ohne Kennwort' => [new PasswordlessTokenException()], + 'Benutzerverwaltung nicht erreichbar' => [new \RuntimeException('directory unavailable')], + ]; + } + + /** + * Lässt sich das hinterlegte Kennwort nicht prüfen (kein Kennwort im Token, + * Ausnahme der Benutzerverwaltung), bleibt die Sitzung bis zur nächsten Prüfung. + * + * @dataProvider providesUncheckableStoredPasswords + * @param \Exception $exception + */ + public function testValidateDavSessionKeepsSessionWhenStoredPasswordCannotBeChecked(\Exception $exception) { + [$userSession, , $manager] = $this->createDavSession(['app_password' => 'APPPW', 'session_token_created' => true]); + $appPasswordToken = $this->createDavToken('user1', 10000 - 301); + $this->withTokens(['APPPW' => $appPasswordToken, 'sessionid' => $this->createDavToken('user1')]); + $this->tokenProvider->expects($this->once())->method('claimCredentialCheck') + ->willReturnCallback(function (DefaultToken $dbToken, $now) { + $dbToken->setLastCheck($now); + return true; + }); + if ($exception instanceof PasswordlessTokenException) { + $this->tokenProvider->method('getPassword')->willThrowException($exception); + $manager->expects($this->never())->method('checkPassword'); + } else { + $this->tokenProvider->method('getPassword')->willReturn('altes-kennwort'); + $manager->expects($this->once())->method('checkPassword')->willThrowException($exception); + $this->logger->expects($this->once())->method('logException')->with($exception); + } + $this->tokenProvider->expects($this->never())->method('invalidateToken'); + $userSession->expects($this->never())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest()); + $this->assertSame(10000, $appPasswordToken->getLastCheck()); + } + + /** + * Hat eine parallele Anfrage die fällige Prüfung schon übernommen, prüft diese + * nicht noch einmal und lässt die Sitzung gelten. + */ + public function testValidateDavSessionLeavesStoredPasswordCheckToParallelRequest() { + [$userSession, , $manager] = $this->createDavSession(['app_password' => 'APPPW', 'session_token_created' => true]); + $this->withTokens(['APPPW' => $this->createDavToken('user1', 10000 - 301), 'sessionid' => $this->createDavToken('user1')]); + $this->tokenProvider->expects($this->once())->method('claimCredentialCheck')->willReturn(false); + $this->tokenProvider->expects($this->never())->method('getPassword'); + $manager->expects($this->never())->method('checkPassword'); + $userSession->expects($this->never())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest()); + } + + /** + * Kennwort-Sitzung, die nur das Cookie schickt: Das bei der Anmeldung + * hinterlegte Kennwort prüft der Server alle last_check_timeout Minuten, wie + * für Browser; nach einer Kennwortänderung endet die Sitzung. + * + * @dataProvider providesStoredPasswordChecks + * @param int $lastCheck + * @param bool $checks + * @param bool $passwordValid + * @param bool $ends + */ + public function testValidateDavSessionChecksPasswordOfCookieOnlyPasswordSession($lastCheck, $checks, $passwordValid, $ends) { + [$userSession, , $manager] = $this->createDavSession(['session_token_created' => true]); + $sessionToken = $this->createDavToken('user1', $lastCheck); + $this->withTokens(['sessionid' => $sessionToken]); + $steps = $this->expectStoredPasswordCheck($manager, $sessionToken, 'sessionid', $checks, $passwordValid); + $userSession->expects($ends ? $this->once() : $this->never())->method('logout'); + if ($ends) { + $this->logger->expects($this->once())->method('info') + ->with($this->stringContains('ended'), $this->callback(function ($context) { + return $context['reason'] === 'password changed'; + })); + } + + $userSession->validateDavSession($this->createDavRequest()); + + $this->assertSame($checks ? ['vermerkt', 'geprüft'] : [], $steps->getArrayCopy()); + } + + /** + * Schickt die Kennwort-Sitzung das Kennwort mit (HTTP-Basic), prüft es + * BasicAuthModule wie bisher alle last_check_timeout Minuten; das hinterlegte + * fragt validateDavSession() dann nicht zusätzlich ab. + */ + public function testValidateDavSessionLeavesPasswordOfBasicRequestToBasicAuthModule() { + [$userSession, , $manager] = $this->createDavSession(['session_token_created' => true]); + $this->withTokens(['sessionid' => $this->createDavToken('user1', 10000 - 301)]); + $this->tokenProvider->expects($this->never())->method('claimCredentialCheck'); + $manager->expects($this->never())->method('checkPassword'); + $this->tokenProvider->expects($this->never())->method('getPassword'); + $userSession->expects($this->never())->method('logout'); + + $userSession->validateDavSession($this->createDavRequest(['PHP_AUTH_USER' => 'user1', 'PHP_AUTH_PW' => 'secret'])); + } + /** * Sitzung unter „Sitzungen“ entfernt: Das Token wird nicht neu angelegt. */ @@ -2366,32 +2572,57 @@ public function testValidateDavSessionOfUnmarkedSessionWithBearer($moduleResult, } public function providesAuthModuleRequests() { + $bearer = function ($token) { + return ['HTTP_AUTHORIZATION' => "Bearer $token"]; + }; + $tokenEndpoint = ['REQUEST_URI' => '/index.php/apps/oauth2/api/v1/token', 'SCRIPT_NAME' => '/index.php']; + $clientBasic = ['HTTP_AUTHORIZATION' => 'Basic Y2xpZW50OnNlY3JldA==']; + $rejected = new \OC\User\LoginException('Invalid token'); + // [Kopf und Pfad, Methode, last_check, Modul gefragt, Antwort des Moduls, Ausgang] return [ - 'derselbe Bearer, eben geprüft' => [['HTTP_AUTHORIZATION' => 'Bearer abc'], 10000, false, null, false], - 'derselbe Bearer nach Rewrite, eben geprüft' => [['REDIRECT_HTTP_AUTHORIZATION' => 'Bearer abc'], 10000, false, null, false], - 'nur Cookie' => [[], 10000, true, null, true], - 'Basic statt Bearer' => [['HTTP_AUTHORIZATION' => 'Basic dXNlcjE6c2VjcmV0'], 10000, true, null, true], - 'erneuerter Bearer' => [['HTTP_AUTHORIZATION' => 'Bearer neu'], 10000, true, 'user1', false], - 'Bearer eines anderen Nutzers' => [['HTTP_AUTHORIZATION' => 'Bearer fremd'], 10000, true, 'user2', true], - 'derselbe Bearer, Prüfung fällig, gilt noch' => [['HTTP_AUTHORIZATION' => 'Bearer abc'], 10000 - 301, true, 'user1', false], - 'derselbe Bearer, Prüfung fällig, widerrufen' => [['HTTP_AUTHORIZATION' => 'Bearer abc'], 10000 - 301, true, new \OC\User\LoginException('Invalid token'), true], - 'derselbe Bearer, Prüfung gerade noch nicht fällig' => [['HTTP_AUTHORIZATION' => 'Bearer abc'], 10000 - 299, false, null, false], + 'derselbe Bearer, eben geprüft' => [$bearer('abc'), 'PROPFIND', 10000, false, null, 'gilt'], + 'derselbe Bearer nach Rewrite, eben geprüft' => [['REDIRECT_HTTP_AUTHORIZATION' => 'Bearer abc'], 'PROPFIND', 10000, false, null, 'gilt'], + 'nur Cookie' => [[], 'PROPFIND', 10000, true, null, 'endet'], + 'nur Cookie, Modul wirft' => [[], 'PROPFIND', 10000, true, $rejected, 'endet'], + 'Basic an WebDAV' => [['HTTP_AUTHORIZATION' => 'Basic dXNlcjE6c2VjcmV0'], 'PROPFIND', 10000, true, null, 'endet'], + 'erneuerter Bearer' => [$bearer('neu'), 'PROPFIND', 10000, true, 'user1', 'gilt'], + 'Bearer eines anderen Nutzers' => [$bearer('fremd'), 'PROPFIND', 10000, true, 'user2', 'endet'], + 'anderer Bearer, vom Modul abgelehnt (etwa der alte nach der Erneuerung)' => [$bearer('alt'), 'PROPFIND', 10000, true, $rejected, 'bleibt'], + 'anderer Bearer, kein Modul kennt ihn' => [$bearer('unbekannt'), 'PROPFIND', 10000, true, null, 'endet'], + 'derselbe Bearer, Prüfung fällig, gilt noch' => [$bearer('abc'), 'PROPFIND', 10000 - 301, true, 'user1', 'gilt'], + 'derselbe Bearer, Prüfung genau fällig' => [$bearer('abc'), 'PROPFIND', 10000 - 300, true, 'user1', 'gilt'], + 'derselbe Bearer, Prüfung fällig, abgelaufen oder widerrufen' => [$bearer('abc'), 'PROPFIND', 10000 - 301, true, $rejected, 'bleibt'], + 'derselbe Bearer, Prüfung fällig, Modul kennt ihn nicht mehr' => [$bearer('abc'), 'PROPFIND', 10000 - 301, true, null, 'endet'], + 'derselbe Bearer, Prüfung gerade noch nicht fällig' => [$bearer('abc'), 'PROPFIND', 10000 - 299, false, null, 'gilt'], + 'Token-Endpunkt, Client-Daten über Basic' => [$tokenEndpoint + $clientBasic, 'POST', 10000, false, null, 'bleibt'], + 'Token-Endpunkt, Client-Daten im Formular' => [$tokenEndpoint, 'POST', 10000, false, null, 'bleibt'], + 'Token-Endpunkt, Prüfung fällig' => [$tokenEndpoint + $clientBasic, 'POST', 10000 - 301, false, null, 'bleibt'], + 'OpenID-Connect-Discovery' => [['REQUEST_URI' => '/index.php/apps/openidconnect/config', 'SCRIPT_NAME' => '/index.php'], 'GET', 10000, false, null, 'bleibt'], + 'GET am Token-Pfad' => [$tokenEndpoint + $clientBasic, 'GET', 10000, true, null, 'endet'], + 'Token-Pfad unter anderem Präfix' => [['REQUEST_URI' => '/index.php/apps/files/apps/oauth2/api/v1/token', 'SCRIPT_NAME' => '/index.php'] + $clientBasic, 'POST', 10000, true, null, 'endet'], ]; } /** - * Sitzung eines Anmeldemoduls (OAuth2): Jede Anfrage muss den bestätigten - * Authorization-Kopf tragen; ein anderer wird sofort geprüft, derselbe nach - * last_check_timeout (5 Minuten). + * Sitzung eines Anmeldemoduls (OAuth2): Jede Anfrage muss einen Authorization- + * Kopf tragen, den das Modul für den Nutzer der Sitzung bestätigt; ein anderer + * Kopf wird sofort geprüft, derselbe nach last_check_timeout (5 Minuten). + * - gilt: bestätigt (Vermerk und Prüfzeit fortgeschrieben, wenn gefragt) + * - bleibt: weder bestätigt noch widerlegt – lehnt das Modul den Kopf ab + * (abgelaufenes Token), weisen WebDAV, OCS und Weboberfläche die Anfrage + * selbst ab; Token-Endpunkt und Discovery brauchen die Sitzung nicht. Die + * Sitzung bleibt unverändert, auch ihre Aktivität. + * - endet: kein oder ein fremder Nutzer * * @dataProvider providesAuthModuleRequests * @param array $server + * @param string $method * @param int $lastCheck * @param bool $asksModule * @param string|\Exception|null $moduleResult Nutzer, Ausnahme oder null - * @param bool $ends + * @param string $outcome gilt, bleibt oder endet */ - public function testValidateDavSessionOfAuthModule(array $server, $lastCheck, $asksModule, $moduleResult, $ends) { + public function testValidateDavSessionOfAuthModule(array $server, $method, $lastCheck, $asksModule, $moduleResult, $outcome) { $module = $this->createMock(IAuthModule::class); $auth = $module->expects($asksModule ? $this->once() : $this->never())->method('auth'); if ($moduleResult instanceof \Exception) { @@ -2407,20 +2638,39 @@ public function testValidateDavSessionOfAuthModule(array $server, $lastCheck, $a [$userSession, $store] = $this->createDavSession(['session_token_created' => true, 'auth_module' => $authModule], [$module]); $sessionToken = $this->createDavToken('user1', $lastCheck); $this->withTokens(['sessionid' => $sessionToken]); - $userSession->expects($ends ? $this->once() : $this->never())->method('logout'); + $userSession->expects($outcome === 'endet' ? $this->once() : $this->never())->method('logout'); + $this->tokenProvider->expects($outcome === 'gilt' ? $this->once() : $this->never())->method('updateTokenActivity'); + $this->tokenProvider->expects($outcome === 'gilt' && $asksModule ? $this->once() : $this->never())->method('updateToken'); - $userSession->validateDavSession($this->createDavRequest($server)); + $userSession->validateDavSession($this->createDavRequest($server, $method)); - if (!$ends && $asksModule) { + if ($outcome === 'gilt' && $asksModule) { $authorization = $server['HTTP_AUTHORIZATION'] ?? ''; $this->assertSame(\hash('sha256', $authorization), $store->get('auth_module')['credential']); $this->assertSame(10000, $sessionToken->getLastCheck()); - } elseif (!$ends) { + } elseif ($outcome !== 'endet') { $this->assertSame($authModule, $store->get('auth_module')); $this->assertSame($lastCheck, $sessionToken->getLastCheck()); } } + /** + * Sitzung von vor dieser Prüfung am Token-Endpunkt: Die Anfrage lässt sie + * unberührt, auch ohne Vermerk – woher sie stammt, zeigt erst die nächste. + */ + public function testValidateDavSessionLeavesSessionFromBeforeTheCheckAtTokenEndpoint() { + [$userSession, $store] = $this->createDavSession([]); + $this->withTokens(['sessionid' => $this->createDavToken('user1')]); + $userSession->expects($this->never())->method('logout'); + $this->tokenProvider->expects($this->never())->method('updateTokenActivity'); + + $userSession->validateDavSession($this->createDavRequest( + ['REQUEST_URI' => '/index.php/apps/oauth2/api/v1/token', 'SCRIPT_NAME' => '/index.php'], + 'POST' + )); + $this->assertFalse($store->exists('session_token_created')); + } + /** * Ist die App des Anmeldemoduls nicht mehr da, lässt sich der Kopf nicht * prüfen: Die Sitzung endet. From a6b9cbc827773d5fa0ef36f77504ea63c10e4787 Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Thu, 22 Oct 2026 16:09:41 +0200 Subject: [PATCH 07/11] =?UTF-8?q?feat(files=5Fversions):=20n=C3=A4chsten?= =?UTF-8?q?=20Schreibvorgang=20ohne=20Version=20(Storage::skipNextVersion)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editoren mit automatischem Zwischenspeichern (oco_office: Autosave von Collabora) erzeugten bei jedem Zwischenspeichern eine Version. Neue Methode Storage::skipNextVersion($path): der nächste Schreibvorgang auf den Pfad legt keine Version an – einmalig und nur in dieser Anfrage. Test testSkipNextVersion. --- apps/files_versions/lib/Storage.php | 22 ++++++++++++++++++++ apps/files_versions/tests/VersioningTest.php | 12 +++++++++++ 2 files changed, 34 insertions(+) diff --git a/apps/files_versions/lib/Storage.php b/apps/files_versions/lib/Storage.php index 6233ee6290..4668b912fc 100644 --- a/apps/files_versions/lib/Storage.php +++ b/apps/files_versions/lib/Storage.php @@ -66,6 +66,9 @@ class Storage { private static $sourcePathAndUser = []; + /** @var array Pfade, deren nächster Schreibvorgang keine Version anlegt */ + private static $skipNextVersion = []; + private static $max_versions_per_interval = [ //first 10sec, one version every 2sec 1 => ['intervalEndsAfter' => 10, 'step' => 2], @@ -164,6 +167,19 @@ public static function getSourcePathAndUser($source) { return [$uid, $path]; } + /** + * Der nächste Schreibvorgang auf $path legt keine Version an – einmalig und nur + * innerhalb dieser Anfrage. Für Editoren, die automatisch zwischenspeichern + * (oco_office: Autosave von Collabora), damit nicht jedes Zwischenspeichern eine + * Version erzeugt; ausdrückliches Speichern legt weiter eine an. + * + * @param string $path Pfad im Dateibaum des Nutzers, wie ihn die Filesystem-Hooks + * liefern (z. B. "/Ordner/Bericht.odt") + */ + public static function skipNextVersion(string $path): void { + self::$skipNextVersion[Filesystem::normalizePath($path)] = true; + } + /** * store a new version of a file. */ @@ -176,6 +192,12 @@ public static function store($filename) { $filename = \substr($filename, 0, \strlen($filename) - 5); } + $normalized = Filesystem::normalizePath($filename); + if (isset(self::$skipNextVersion[$normalized])) { + unset(self::$skipNextVersion[$normalized]); + return false; + } + // we don't support versioned directories if (Filesystem::is_dir($filename)) { return false; diff --git a/apps/files_versions/tests/VersioningTest.php b/apps/files_versions/tests/VersioningTest.php index 1f804ca6c7..8a8add0c9d 100644 --- a/apps/files_versions/tests/VersioningTest.php +++ b/apps/files_versions/tests/VersioningTest.php @@ -199,6 +199,18 @@ private function overwriteConfig($saveVersionMetadata=false, $versionsRetentionO Hooks::connectHooks(); } + public function testSkipNextVersion(): void { + $this->overwriteConfig(); // verbindet die Versions-Hooks + Filesystem::file_put_contents('autosave.txt', 'original'); + // Zwischenspeichern eines Editors: keine Version + \OCA\Files_Versions\Storage::skipNextVersion('/autosave.txt'); + Filesystem::file_put_contents('autosave.txt', 'autosave'); + $this->assertCount(0, \OCA\Files_Versions\Storage::getVersions($this->user1, '/autosave.txt')); + // gilt nur einmal: der nächste Schreibvorgang legt wieder eine Version an + Filesystem::file_put_contents('autosave.txt', 'gespeichert'); + $this->assertCount(1, \OCA\Files_Versions\Storage::getVersions($this->user1, '/autosave.txt')); + } + public function metaDataEnabledProvider(): array { return [ 'metaDisabled' => [false], From f4c475a7268ae89b54458b0992ae3e2aa46cb7c7 Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Thu, 22 Oct 2026 16:09:41 +0200 Subject: [PATCH 08/11] fix(log): access_token, requesttoken und password in der protokollierten Adresse maskieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Kern schreibt die Anfrage-Adresse in jeden Protokolleintrag. Collabora hängt das WOPI-Zugriffstoken als access_token an jede Anfrage – jeder Eintrag während eines WOPI-Aufrufs hätte das Token im Klartext enthalten. Owncloud::sanitizeUrl() maskiert die Werte, Datei- und Syslog-Protokoll nutzen sie. --- lib/private/Log/Owncloud.php | 11 ++++++++++- lib/private/Log/Syslog.php | 2 +- tests/lib/Log/OwncloudTest.php | 11 ++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/private/Log/Owncloud.php b/lib/private/Log/Owncloud.php index b2017e5461..fe0d4b2978 100644 --- a/lib/private/Log/Owncloud.php +++ b/lib/private/Log/Owncloud.php @@ -74,6 +74,15 @@ public static function write($app, $message, $level, $conditionalLogFile = null) self::writeExtra($app, $message, $level, $conditionalLogFile, []); } + /** + * Geheimnisse aus der protokollierten Adresse entfernen: WOPI-Zugriffstoken + * (Collabora hängt access_token an jede Anfrage), CSRF-Token und Kennwörter + * stünden sonst im Klartext in jedem Protokolleintrag dieser Anfrage. + */ + public static function sanitizeUrl(string $url): string { + return (string)\preg_replace('/([?&](?:access_token|requesttoken|password)=)[^&#]*/i', '$1***', $url); + } + public static function writeExtra($app, $message, $level, $conditionalLogFile, $extraFields = []) { $config = \OC::$server->getSystemConfig(); @@ -102,7 +111,7 @@ public static function writeExtra($app, $message, $level, $conditionalLogFile, $ $request = \OC::$server->getRequest(); $reqId = $request->getId(); $remoteAddr = $request->getRemoteAddress(); - $url = ($request->getRequestUri() !== '') ? $request->getRequestUri() : '--'; + $url = ($request->getRequestUri() !== '') ? self::sanitizeUrl($request->getRequestUri()) : '--'; $method = \is_string($request->getMethod()) ? $request->getMethod() : '--'; if (\OC::$server->getConfig()->getSystemValue('installed', false)) { $user = (\OC_User::getUser()) ? \OC_User::getUser() : '--'; diff --git a/lib/private/Log/Syslog.php b/lib/private/Log/Syslog.php index 7e0de1d0ec..d41c36ef64 100644 --- a/lib/private/Log/Syslog.php +++ b/lib/private/Log/Syslog.php @@ -69,7 +69,7 @@ public static function write($app, $message, $level) { '%user%' => $user, '%app%' => $app, '%method%' => \is_string($request->getMethod()) ? $request->getMethod() : '--', - '%url%' => ($request->getRequestUri() !== '') ? $request->getRequestUri() : '--', + '%url%' => ($request->getRequestUri() !== '') ? Owncloud::sanitizeUrl($request->getRequestUri()) : '--', '%message%' => $message ]; diff --git a/tests/lib/Log/OwncloudTest.php b/tests/lib/Log/OwncloudTest.php index e3486c0bb9..37f9eaa580 100644 --- a/tests/lib/Log/OwncloudTest.php +++ b/tests/lib/Log/OwncloudTest.php @@ -53,7 +53,16 @@ protected function tearDown(): void { Owncloud::init(); parent::tearDown(); } - + + public function testSanitizeUrlMasksTokens(): void { + $this->assertSame( + '/index.php/apps/oco_office/wopi/files/7?access_token=***&access_token_ttl=0', + Owncloud::sanitizeUrl('/index.php/apps/oco_office/wopi/files/7?access_token=4f1a9c&access_token_ttl=0') + ); + $this->assertSame('/x?a=1&requesttoken=***&password=***', Owncloud::sanitizeUrl('/x?a=1&requesttoken=abc&password=geheim')); + $this->assertSame('/remote.php/dav/files/u/a.txt', Owncloud::sanitizeUrl('/remote.php/dav/files/u/a.txt')); + } + public function testMicrosecondsLogTimestamp() { $config = \OC::$server->getConfig(); # delete old logfile From 602d66d819e319e055156ab0c89ff33c74a8eb42 Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Wed, 28 Oct 2026 09:46:04 +0100 Subject: [PATCH 09/11] =?UTF-8?q?docs(changelog):=20Fragmente=20f=C3=BCr?= =?UTF-8?q?=2011.0.25?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit f4c475a726 (access_token, requesttoken und password in der protokollierten Adresse maskieren) und a6b9cbc827 (Storage::skipNextVersion) kamen ohne Fragment unter changelog/unreleased. log-url-secrets-masked (Security) nennt auch die Grenzen der Maskierung, versions-skip-next-version (Enhancement) die neue Methode. Die übrigen Änderungen für 11.0.25 bringen ihr Fragment mit. --- changelog/unreleased/log-url-secrets-masked | 18 ++++++++++++++++++ .../unreleased/versions-skip-next-version | 11 +++++++++++ 2 files changed, 29 insertions(+) create mode 100644 changelog/unreleased/log-url-secrets-masked create mode 100644 changelog/unreleased/versions-skip-next-version diff --git a/changelog/unreleased/log-url-secrets-masked b/changelog/unreleased/log-url-secrets-masked new file mode 100644 index 0000000000..cdfa105434 --- /dev/null +++ b/changelog/unreleased/log-url-secrets-masked @@ -0,0 +1,18 @@ +Security: Tokens and passwords in the request address are masked in the log + +Every entry in owncloud.log and in syslog records the address of the request +that wrote it, including the query string. WOPI clients such as Collabora +Online append the access token of the opened document as access_token to +every request, so each entry written during such a request contained a valid +token in plain text. The same applied to requests that carried requesttoken +or password in their query string. The logged address now shows *** instead +of the values of the query parameters access_token, requesttoken and +password, for the log types owncloud and syslog. The office app oco_office +relies on this and does not open documents on a server without it. + +Only these three query parameters are masked. Tokens in the path of an +address, for example in the links to reset a password or to confirm a mail +address, other query parameters such as log_secret, and the text of messages +and exceptions are logged as before. Log files written before the update and +the access logs of the web server and of Collabora Online may still contain +such tokens; rotate or clean them up. diff --git a/changelog/unreleased/versions-skip-next-version b/changelog/unreleased/versions-skip-next-version new file mode 100644 index 0000000000..9b466c1ab3 --- /dev/null +++ b/changelog/unreleased/versions-skip-next-version @@ -0,0 +1,11 @@ +Enhancement: Apps can skip the version of the next write to a file + +Editors that save a document automatically while it is open, such as +Collabora Online in the office app oco_office, created a new version of the +file with every automatic save. With the new method +OCA\Files_Versions\Storage::skipNextVersion($path) an app tells the versions +app that the next write to the given path creates no version. This applies +once and only within the current request: the following write creates a +version as before, so a save requested by the user still does. The path is +the one in the file tree of the user, as the filesystem hooks pass it, for +example /Documents/Report.odt. Writes without this call are not affected. From 30070c1fdcc02c9db3348a1c0b864293656d5789 Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Wed, 28 Oct 2026 09:50:39 +0100 Subject: [PATCH 10/11] chore(release): prepare 11.0.25 --- CHANGELOG.md | 160 +++++++++++++++++++++++++++++ docs/administration/apps-market.md | 2 +- docs/developer/index.md | 2 +- docs/developer/release-workflow.md | 10 +- docs/index.md | 2 +- docs/installation/linux-server.md | 8 +- docs/plugins/index.md | 2 +- version.php | 7 +- 8 files changed, 177 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b07ca4a23..6f886f08e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Table of Contents +* [Changelog for owncloud.online 11.0.25](#changelog-for-owncloudonline-11025-2026-10-28) * [Changelog for owncloud.online 11.0.24](#changelog-for-owncloudonline-11024-2026-10-21) * [Changelog for owncloud.online 11.0.23](#changelog-for-owncloudonline-11023-2026-10-16) * [Changelog for owncloud.online 11.0.22](#changelog-for-owncloudonline-11022-2026-10-02) @@ -41,6 +42,165 @@ * [Changelog for 10.4.1](#changelog-for-owncloud-core-1041-2020-03-30) * [Changelog for 10.4.0](#changelog-for-owncloud-core-1040-2020-02-10) * [Changelog for 10.3.2](#changelog-for-owncloud-core-1032-2019-12-04) +# Changelog for owncloud.online [11.0.25] (2026-10-28) + +The following sections list the changes in owncloud.online 11.0.25 relevant to +admins and users. + +[11.0.25]: https://github.com/BWTECH-github/owncloud.online + +## Summary + +* Security - Tokens und Kennwörter in der protokollierten Adresse maskiert: [#f4c475a](https://github.com/BWTECH-github/owncloud.online/commit/f4c475a) +* Security - Widerruf und Kennwortwechsel beenden die Sitzung eines Sync-Clients: [#b8a070a](https://github.com/BWTECH-github/owncloud.online/commit/b8a070a), [#53397d1](https://github.com/BWTECH-github/owncloud.online/commit/53397d1) +* Enhancement - Apps können die Version des nächsten Schreibvorgangs auslassen: [#a6b9cbc](https://github.com/BWTECH-github/owncloud.online/commit/a6b9cbc) +* Bugfix - Markt-App ist direkt nach der Neuinstallation eingeschaltet: [#ed6b897](https://github.com/BWTECH-github/owncloud.online/commit/ed6b897) +* Bugfix - Anmeldung ohne Benutzer- oder Kennwortfeld schreibt keine PHP-Deprecation mehr: [#467d479](https://github.com/BWTECH-github/owncloud.online/commit/467d479) +* Bugfix - Fehler vor dem Versand der Reset-Mail blockiert die nächste Anforderung nicht mehr: [#07216ae](https://github.com/BWTECH-github/owncloud.online/commit/07216ae) +* Bugfix - owncloud.online statt alter Produktnamen in den Übersetzungen: [#f4e2b9a](https://github.com/BWTECH-github/owncloud.online/commit/f4e2b9a) + +## Details + +* Security - Tokens und Kennwörter in der protokollierten Adresse maskiert + + Jeder Eintrag in `owncloud.log` und im Syslog enthält die Adresse der + Anfrage, die ihn schrieb, samt Abfrageteil. WOPI-Clients wie Collabora + Online hängen das Zugriffstoken des geöffneten Dokuments als + `access_token` an jede Anfrage. Jeder Eintrag während einer solchen + Anfrage enthielt das gültige Token damit im Klartext, ebenso Einträge zu + Anfragen mit `requesttoken` oder `password` im Abfrageteil. In der + protokollierten Adresse steht jetzt `***` statt der Werte dieser drei + Parameter, bei den Protokollarten `owncloud` und `syslog`. Die Office-App + oco_office setzt diese Maskierung voraus und öffnet auf einem Server ohne + sie keine Dokumente. + + Bekannte Einschränkung: Maskiert werden nur diese drei Abfrageparameter + in der Adresse des Eintrags. Tokens im Pfad einer Adresse (etwa in den + Links zum Zurücksetzen des Kennworts oder zum Bestätigen einer + Mailadresse), andere Parameter wie `log_secret` und der Text von + Meldungen und Ausnahmen stehen weiter unverändert im Protokoll. + Protokolle von vor dem Update sowie die Zugriffsprotokolle des Webservers + und von Collabora Online können solche Tokens noch enthalten; sie sollten + rotiert oder bereinigt werden. + +* Security - Widerruf und Kennwortwechsel beenden die Sitzung eines Sync-Clients + + Sync-Clients schicken mit jeder Anfrage ihr Sitzungs-Cookie mit. Wurde + ein App-Passwort in den persönlichen Einstellungen widerrufen, + synchronisierte ein Client damit weiter: Anfragen mit dem widerrufenen + App-Passwort und dem Cookie und Anfragen nur mit dem Cookie bekamen 207. + Sitzungen, die ein Client selbst begonnen hatte (WebDAV, HTTP Basic, + OAuth2), prüfte der Server nie gegen die Anmeldung, mit der sie begannen, + und jede Anfrage verlängerte sie. „Sitzung entfernen“ unter „Sitzungen“ + wirkte nicht, weil die nächste Anfrage das Sitzungstoken neu anlegte. + Nach dem Widerruf einer Desktop-Freigabe (OAuth2) galt die Sitzung mit + dem Cookie allein weiter, und ein Kennwortwechsel beendete keine dieser + Sitzungen. + + Der Server prüft diese Sitzungen jetzt vor jeder Anfrage, wie + Browser-Sitzungen. Ein widerrufenes App-Passwort beendet die Sitzung mit + der nächsten Anfrage, auch wenn der Client nur das Cookie schickt, ebenso + das Entfernen der Sitzung unter „Sitzungen“. Alle `last_check_timeout` + Minuten (App-Wert `core`, Vorgabe 5) prüft der Server das Kennwort, das + in der Sitzung oder im App-Passwort hinterlegt ist; gilt es nicht mehr, + endet die Sitzung. Bei OAuth2 und OpenID Connect braucht jede Anfrage + einen Authorization-Kopf, den das Anmeldemodul für den Nutzer der Sitzung + bestätigt: Einen anderen oder fehlenden Kopf prüft es sofort, denselben + wieder nach `last_check_timeout` Minuten. Ein abgelaufenes Token beendet + die Sitzung bei WebDAV nicht, und Token-Erneuerung sowie + OpenID-Connect-Discovery lassen sie unberührt. + + Verhaltensänderungen nach dem Update: Sitzungen von vor dem Update, die + ohne App-Passwort nur das Cookie schicken, melden sich einmal neu an. + App-Passwörter, die vor einem Kennwortwechsel entstanden, prüft der + Server jetzt auch in laufenden Sitzungen; sie werden mit der nächsten + Prüfung ungültig, wie bisher schon bei einer neuen Anmeldung damit, und + die Geräte brauchen ein neues App-Passwort. Ausgenommen sind Konten aus + LDAP, deren App-Passwörter widerrufen werden müssen. Anfragen, die zu + einer per OAuth2 begonnenen Sitzung nur das Cookie ohne Token schicken, + bekommen 401; Desktop- und Mobil-Clients senden das Token immer mit. Jede + Prüfung mit einem veralteten Kennwort steht als „Login failed“ im + Protokoll und zählt für die Bremse gegen Passwort-Raten. Hinter einer + gemeinsamen Adresse (NAT) können viele solcher Prüfungen nach einem + Kennwortwechsel die Bremse für alle Anmeldungen von dieser Adresse + auslösen. + + Eine beendete Sitzung wird abgemeldet und ihr Sitzungstoken gelöscht; das + Protokoll meldet auf Stufe info „Client session of `` ended: + ``“. Browser-Sitzungen, öffentliche Links, Gäste und die frühe + Freigabe der Sitzungssperre für Sync-Clients bleiben unverändert. Die + Doku beschreibt das Verhalten unter Fehlersuche im Abschnitt „Client nach + Widerruf abgemeldet“. + + Bekannte Einschränkung: Hat eine Sitzung mit Kennwort ihr Sitzungstoken + mit einer Anfrage bekommen, die nur das Cookie trug, ist darin kein + Kennwort hinterlegt, und ein Kennwortwechsel beendet sie nicht. Dann + hilft „Sitzung entfernen“ unter „Sitzungen“. + +* Enhancement - Apps können die Version des nächsten Schreibvorgangs auslassen + + Editoren, die ein geöffnetes Dokument selbst zwischenspeichern, etwa + Collabora Online in der Office-App oco_office, legten bei jedem + Zwischenspeichern eine neue Version der Datei an. Mit der neuen Methode + `OCA\Files_Versions\Storage::skipNextVersion($path)` teilt eine App der + Versionsverwaltung mit, dass der nächste Schreibvorgang auf diesen Pfad + keine Version anlegt, einmalig und nur innerhalb der laufenden Anfrage. + Der folgende Schreibvorgang legt wieder eine an, ausdrückliches Speichern + also weiterhin. Der Pfad ist der im Dateibaum des Nutzers, wie ihn die + Dateisystem-Hooks liefern (etwa `/Dokumente/Bericht.odt`). Für + Schreibvorgänge ohne diesen Aufruf ändert sich nichts. + +* Bugfix - Markt-App ist direkt nach der Neuinstallation eingeschaltet + + Eine Neuinstallation trug `apps-external` erst nach der Einrichtung der + mitgelieferten Apps als App-Verzeichnis in die `config.php` ein. Der + Installer fand die Markt-App deshalb nicht, und sie blieb aus, bis das + erste Update sie mit `occ upgrade` einschaltete. Die App-Verzeichnisse + stehen jetzt vorher fest, und `occ maintenance:install` wie der + Web-Installer schalten die Markt-App ein, wie es die + Installationsanleitung beschreibt. Bestehende Installationen, Updates und + vorgegebene `apps_paths` ändern sich nicht. + + Die Markt-App fragt den Katalog unter `appstoreurl` ab (Vorgabe + `https://marketplace.owncloud.online`), einmal am Tag auch ohne Zutun + eines Admins über einen Hintergrundjob, der nach App-Updates sucht. Wer + das nicht möchte, setzt `appstoreurl` auf `local` oder schaltet die App + mit `occ app:disable market` ab; ein späteres `occ upgrade` schaltet sie + dann nicht wieder ein. + +* Bugfix - Anmeldung ohne Benutzer- oder Kennwortfeld schreibt keine PHP-Deprecation mehr + + Ein Anmeldeformular ohne Feld für Benutzer oder Kennwort, oder mit einem + davon als Liste (`user[]=x`), erreichte die Kennwortprüfung mit null, und + PHP schrieb für jedes der beiden Felder „Passing null to parameter“ aus + `str_replace()` ins Protokoll. Ein fehlendes Feld gilt jetzt als leer. + Kommt ein Benutzername oder Kennwort auf anderem Weg als Liste an (etwa + über OCS oder den Token-Endpunkt), scheitert die Prüfung sofort, bevor + die Bremse gegen Passwort-Raten oder ein Benutzer-Backend die Liste + sieht. Die Anmeldung scheitert in beiden Fällen wie bisher. + +* Bugfix - Fehler vor dem Versand der Reset-Mail blockiert die nächste Anforderung nicht mehr + + Das Zurücksetzen des Kennworts speicherte sein Token, bevor die Mail + zusammengestellt war. Scheiterte das Zusammenstellen, etwa beim Rendern + der Vorlagen, blieb das Token gespeichert, und jede neue Anforderung in + den nächsten fünf Minuten galt als Wiederholung: Es ging keine Mail + hinaus, die Seite meldete aber Erfolg. Das Token wird jetzt erst + unmittelbar vor der Übergabe an den Mailserver gespeichert. Ein solcher + Fehler lässt den Stand von vor der Anforderung stehen, ein älterer, noch + gültiger Link bleibt also gültig. Das ergänzt die Korrektur zu „Erneut + anfordern“ aus 11.0.24. + +* Bugfix - owncloud.online statt alter Produktnamen in den Übersetzungen + + In 68 Texten der Übersetzungen von Kern und mitgelieferten Apps stand + noch der alte Produktname, teils falsch geschrieben oder in kyrillischer + Umschrift, etwa im deutschen Hinweis zum Einzelbenutzermodus und in den + Hinweisen zu vertrauenswürdigen und föderierten Servern. Sie nennen jetzt + owncloud.online, jeweils in der `.json`- und der `.js`-Datei der Sprache. + Beispielpfade im Hinweis zum Teilen und Platzhalter bleiben unverändert. + Ein Test durchsucht alle Übersetzungen nach den alten Schreibweisen. + # Changelog for owncloud.online [11.0.24] (2026-10-21) The following sections list the changes in owncloud.online 11.0.24 relevant to diff --git a/docs/administration/apps-market.md b/docs/administration/apps-market.md index 2e14ae5e37..6b71ad75a1 100644 --- a/docs/administration/apps-market.md +++ b/docs/administration/apps-market.md @@ -206,7 +206,7 @@ Die mitgelieferten Apps kommen mit dem Server. Sie stecken im Release-Archiv und werden nicht einzeln nachinstalliert, sondern mit dem Server aktualisiert. Zu jedem Release gehören außerdem `SHA256SUMS.txt` zur Prüfung der Archive, `sbom-owncloud-online-.cdx.json`, `release-manifest.json` und -`removed-release-files.txt`. Aktuelle Fassung ist 11.0.24. +`removed-release-files.txt`. Aktuelle Fassung ist 11.0.25. ## Aktivieren und abschalten diff --git a/docs/developer/index.md b/docs/developer/index.md index b5366bfb8a..8e36ade738 100644 --- a/docs/developer/index.md +++ b/docs/developer/index.md @@ -9,7 +9,7 @@ ausführt, und der Weg, auf dem eine Änderung eingereicht wird. Der Quelltext liegt unter , fertige Pakete unter . Aktuelle Fassung -ist 11.0.24 (`version.php`). +ist 11.0.25 (`version.php`). ## Aufbau des Repositorys diff --git a/docs/developer/release-workflow.md b/docs/developer/release-workflow.md index 1ba43c5d41..39b7b5d5da 100644 --- a/docs/developer/release-workflow.md +++ b/docs/developer/release-workflow.md @@ -7,7 +7,7 @@ wird es nicht von Hand: Ein Git-Tag stößt den Workflow legt Prüfsummen, eine Stückliste (SBOM) und ein Manifest daneben und veröffentlicht alles als GitHub-Release. Fertige Releases stehen unter , die aktuelle -Fassung ist 11.0.24. +Fassung ist 11.0.25. Diese Seite beschreibt den Weg vom Versionsstand im Repository bis zum geprüften Archiv auf dem Server. Wer nichts baut, sondern nur ein @@ -23,7 +23,7 @@ Release-Archive, wenn der Bau ohne Tag angestoßen wird. | Feld | Bedeutung | | --- | --- | | `$OC_Version` | Vierstellige Fassung, etwa `[11, 0, 13, 0]`. Die vierte Stelle ist laut Kommentar in der Datei ausdrücklich **nicht** Teil der öffentlichen Versionsnummer, sondern löst Datenbank-Aktualisierungen zwischen Vorabfassungen aus | -| `$OC_VersionString` | Die lesbare Version, etwa `11.0.24`. Diesen Wert liest der Release-Workflow aus, wenn ihm keine Version übergeben wurde | +| `$OC_VersionString` | Die lesbare Version, etwa `11.0.25`. Diesen Wert liest der Release-Workflow aus, wenn ihm keine Version übergeben wurde | | `$OC_VersionCanBeUpgradedFrom` | Älteste Stände, von denen aus eine Aktualisierung erlaubt ist | | `$OC_Channel` | Auslieferungskanal, im Repository `bwtech`. Der Bau überschreibt ihn mit dem Wert von `RELEASE_CHANNEL` | | `$OC_Build` | Im Repository leer. Der Bau trägt hier Zeitstempel und Commit ein | @@ -80,8 +80,8 @@ ist, wird also trotzdem gebaut; prüfen Sie vorher den letzten Lauf. Der Workflow reagiert auf jeden Tag, der mit `v` beginnt: ```bash -git tag -a v11.0.24 -m "owncloud.online 11.0.24" -git push origin v11.0.24 +git tag -a v11.0.25 -m "owncloud.online 11.0.25" +git push origin v11.0.25 ``` Damit ist der Bau angestoßen. Die Version ermittelt der Workflow aus dem @@ -159,7 +159,7 @@ Für die Herkunft gibt es die Bau-Bescheinigung (Sigstore, schlüssellos über OIDC), die der Workflow für die Archive und die SBOM ausstellt: ```bash -gh attestation verify owncloud-online-11.0.24.tar.gz \ +gh attestation verify owncloud-online-11.0.25.tar.gz \ --repo BWTECH-github/owncloud.online ``` diff --git a/docs/index.md b/docs/index.md index 0946832fe8..891022891d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ Updates, die eigenen Plugins, das Market-Backend und die Clients. | Bereich | Stand | | --- | --- | -| Server-Version | `11.0.24` | +| Server-Version | `11.0.25` | | PHP-Zielversion | PHP 8.4 | | Repository | | | Stand | 13. August 2026 | diff --git a/docs/installation/linux-server.md b/docs/installation/linux-server.md index af9a1d91cc..dc8fe74da8 100644 --- a/docs/installation/linux-server.md +++ b/docs/installation/linux-server.md @@ -121,7 +121,7 @@ bricht bei mehreren gleichzeitigen Zugriffen ein. Näheres unter Die Releases liegen unter . Der Tag eines -Release lautet `v`, aktuell **11.0.24**. +Release lautet `v`, aktuell **11.0.25**. Ein Release enthält: @@ -138,8 +138,8 @@ Archiv und Prüfsummendatei herunterladen: ```bash cd /tmp -curl -fLO https://github.com/BWTECH-github/owncloud.online/releases/download/v11.0.24/owncloud-online-11.0.24.tar.gz -curl -fLO https://github.com/BWTECH-github/owncloud.online/releases/download/v11.0.24/SHA256SUMS.txt +curl -fLO https://github.com/BWTECH-github/owncloud.online/releases/download/v11.0.25/owncloud-online-11.0.25.tar.gz +curl -fLO https://github.com/BWTECH-github/owncloud.online/releases/download/v11.0.25/SHA256SUMS.txt ``` Prüfsumme vergleichen — der Befehl muss `OK` ausgeben: @@ -160,7 +160,7 @@ entsteht es unterhalb des Zielpfades und wird danach umbenannt: ```bash sudo mkdir -p /var/www -sudo tar -xzf /tmp/owncloud-online-11.0.24.tar.gz -C /var/www +sudo tar -xzf /tmp/owncloud-online-11.0.25.tar.gz -C /var/www sudo mv /var/www/owncloud /var/www/owncloud.online ``` diff --git a/docs/plugins/index.md b/docs/plugins/index.md index 73c6bb8bbb..2769c00096 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -79,7 +79,7 @@ erklären dort zugleich, mit welcher Serverfassung sie zusammenarbeiten: ``` -Die eigene Serverfassung — derzeit **11.0.24** — zeigt: +Die eigene Serverfassung — derzeit **11.0.25** — zeigt: ```bash sudo -u www-data php8.4 occ status diff --git a/version.php b/version.php index 4cbd3d8c51..647aa3ca2c 100644 --- a/version.php +++ b/version.php @@ -23,8 +23,9 @@ * * @copyright Copyright (c) 2026, BW-Tech GmbH * - * Modified by BW-Tech GmbH on 2026-10-21. + * Modified by BW-Tech GmbH on 2026-10-28. * Changes: + * - prepare 11.0.25 * - prepare 11.0.24 * - prepare 11.0.23 * - prepare 11.0.22 @@ -39,10 +40,10 @@ // We only can count up. The 4. digit is only for the internal patch-level to trigger DB upgrades // between betas, final and RCs. This is _not_ the public version number. Reset minor/patch-level // when updating major/minor version number. -$OC_Version = [11, 0, 24, 0]; +$OC_Version = [11, 0, 25, 0]; // The human-readable string -$OC_VersionString = '11.0.24'; +$OC_VersionString = '11.0.25'; $OC_VersionCanBeUpgradedFrom = [[8, 2, 11],[9, 0, 9],[9, 1]]; From 658f978e11e3352b54717bb18c89b14ffc199feb Mon Sep 17 00:00:00 2001 From: BWTECH-github Date: Wed, 28 Oct 2026 11:53:17 +0100 Subject: [PATCH 11/11] =?UTF-8?q?docs(session):=20Kennwortwechsel=20sperrt?= =?UTF-8?q?=20Sitzungsinhaber=20nicht=20verl=C3=A4sslich=20aus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Doku, CHANGELOG und Changelog-Fragment zu „Widerruf und Kennwortwechsel beenden die Sitzung eines Sync-Clients“ versprachen mehr, als der Code hält. Ein Kennwortwechsel beendet die Sitzungen von Clients, die sich regulär mit dem Kennwort anmelden, sperrt aber niemanden verlässlich aus, der eine Sitzung hält: Sitzungen und App-Passwörter ohne hinterlegtes Kennwort überstehen ihn, und Anfragen mit HTTP-Basic-Kopf überlässt die neue Prüfung der Anmeldung mit HTTP-Basic. - troubleshooting.md: Einschränkungen beim Kennwortwechsel benannt, neuer Abschnitt „Gerät verloren oder Zugangsdaten abgegriffen“ mit dem Vorgehen (Kennwort ändern, übrige Sitzungen entfernen, App-Passwörter und OAuth2-Freigaben widerrufen, bei Bedarf Konto deaktivieren). „Oder das Kennwort ändern“ als gleichwertiges Mittel entfernt. - CHANGELOG.md 11.0.25: Aussage zu App-Passwörtern nach einem Kennwortwechsel eingeschränkt, bekannte Einschränkung erweitert. - changelog/unreleased/revoke-ends-client-sessions: Typ Security wie im CHANGELOG, Satz zu HTTP-Basic korrigiert, Einschränkungen und Bremse gegen Passwort-Raten ergänzt. Keine Code-Änderung. --- CHANGELOG.md | 49 ++++++++++------- .../unreleased/revoke-ends-client-sessions | 24 +++++++-- docs/administration/troubleshooting.md | 54 +++++++++++++++---- 3 files changed, 95 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f886f08e5..6ed11165c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,9 +102,11 @@ admins and users. der nächsten Anfrage, auch wenn der Client nur das Cookie schickt, ebenso das Entfernen der Sitzung unter „Sitzungen“. Alle `last_check_timeout` Minuten (App-Wert `core`, Vorgabe 5) prüft der Server das Kennwort, das - in der Sitzung oder im App-Passwort hinterlegt ist; gilt es nicht mehr, - endet die Sitzung. Bei OAuth2 und OpenID Connect braucht jede Anfrage - einen Authorization-Kopf, den das Anmeldemodul für den Nutzer der Sitzung + im App-Passwort oder, bei Anfragen nur mit dem Cookie, in der Sitzung + hinterlegt ist; gilt es nicht mehr, endet die Sitzung. Anfragen mit + HTTP-Basic-Kopf überlässt er wie bisher der Anmeldung mit HTTP-Basic. + Bei OAuth2 und OpenID Connect braucht jede Anfrage einen + Authorization-Kopf, den das Anmeldemodul für den Nutzer der Sitzung bestätigt: Einen anderen oder fehlenden Kopf prüft es sofort, denselben wieder nach `last_check_timeout` Minuten. Ein abgelaufenes Token beendet die Sitzung bei WebDAV nicht, und Token-Erneuerung sowie @@ -112,18 +114,20 @@ admins and users. Verhaltensänderungen nach dem Update: Sitzungen von vor dem Update, die ohne App-Passwort nur das Cookie schicken, melden sich einmal neu an. - App-Passwörter, die vor einem Kennwortwechsel entstanden, prüft der - Server jetzt auch in laufenden Sitzungen; sie werden mit der nächsten - Prüfung ungültig, wie bisher schon bei einer neuen Anmeldung damit, und - die Geräte brauchen ein neues App-Passwort. Ausgenommen sind Konten aus - LDAP, deren App-Passwörter widerrufen werden müssen. Anfragen, die zu - einer per OAuth2 begonnenen Sitzung nur das Cookie ohne Token schicken, - bekommen 401; Desktop- und Mobil-Clients senden das Token immer mit. Jede - Prüfung mit einem veralteten Kennwort steht als „Login failed“ im - Protokoll und zählt für die Bremse gegen Passwort-Raten. Hinter einer - gemeinsamen Adresse (NAT) können viele solcher Prüfungen nach einem - Kennwortwechsel die Bremse für alle Anmeldungen von dieser Adresse - auslösen. + App-Passwörter, in denen ein Kennwort von vor einem Kennwortwechsel + hinterlegt ist, prüft der Server jetzt auch in laufenden Sitzungen; sie + werden mit der nächsten Prüfung ungültig, wie bisher schon bei einer + neuen Anmeldung damit, und die Geräte brauchen ein neues App-Passwort. + Ausgenommen sind Konten aus LDAP und App-Passwörter ohne hinterlegtes + Kennwort, etwa solche, die in einer Sitzung ohne hinterlegtes Kennwort + angelegt wurden: Sie bleiben gültig und müssen widerrufen werden. + Anfragen, die zu einer per OAuth2 begonnenen Sitzung nur das Cookie ohne + Token schicken, bekommen 401; Desktop- und Mobil-Clients senden das Token + immer mit. Jede Prüfung mit einem veralteten Kennwort steht als + „Login failed“ im Protokoll und zählt für die Bremse gegen Passwort-Raten. + Hinter einer gemeinsamen Adresse (NAT) können viele solcher Prüfungen + nach einem Kennwortwechsel die Bremse für alle Anmeldungen von dieser + Adresse auslösen. Eine beendete Sitzung wird abgemeldet und ihr Sitzungstoken gelöscht; das Protokoll meldet auf Stufe info „Client session of `` ended: @@ -132,10 +136,17 @@ admins and users. Doku beschreibt das Verhalten unter Fehlersuche im Abschnitt „Client nach Widerruf abgemeldet“. - Bekannte Einschränkung: Hat eine Sitzung mit Kennwort ihr Sitzungstoken - mit einer Anfrage bekommen, die nur das Cookie trug, ist darin kein - Kennwort hinterlegt, und ein Kennwortwechsel beendet sie nicht. Dann - hilft „Sitzung entfernen“ unter „Sitzungen“. + Bekannte Einschränkung: Ein Kennwortwechsel beendet die Sitzungen von + Clients, die sich regulär mit dem Kennwort anmelden, sperrt aber + niemanden verlässlich aus, der eine Sitzung (ihr Cookie) hält. Hat eine + Sitzung mit Kennwort ihr Sitzungstoken mit einer Anfrage bekommen, die + nur das Cookie trug, ist darin kein Kennwort hinterlegt, und ein + Kennwortwechsel beendet sie nie. Bei Verdacht auf ein verlorenes Gerät + oder abgegriffene Zugangsdaten deshalb das Kennwort ändern und + zusätzlich alle übrigen Sitzungen unter „Sitzungen“ entfernen, die + App-Passwörter prüfen und widerrufen und bei Bedarf das Konto + vorübergehend deaktivieren. Die Doku beschreibt das im Abschnitt „Gerät + verloren oder Zugangsdaten abgegriffen“. * Enhancement - Apps können die Version des nächsten Schreibvorgangs auslassen diff --git a/changelog/unreleased/revoke-ends-client-sessions b/changelog/unreleased/revoke-ends-client-sessions index 05139550a3..d88666c05a 100644 --- a/changelog/unreleased/revoke-ends-client-sessions +++ b/changelog/unreleased/revoke-ends-client-sessions @@ -1,4 +1,4 @@ -Bugfix: Revoked access ends the session of a sync client +Security: Revocation and password change end the session of a sync client Sync clients send their session cookie along with every request. After an app password was revoked in the personal settings, a client that used it kept @@ -19,11 +19,13 @@ These sessions are now checked before every request, like browser sessions: core/last_check_timeout minutes (default 5) the password the app password was created with is checked, as a login with the app password does; after a password change the app password becomes invalid and the session ends. - Accounts from LDAP are exempt, as on login. + Accounts from LDAP are exempt, as on login, and so are app passwords + without a stored password, for example ones created in a session without a + stored password: they stay valid and have to be revoked. - Password: when the client sends only the cookie, the password stored with the session is checked every core/last_check_timeout minutes; after a - password change the session ends. Requests that send the password are - checked by the HTTP Basic login as before. + password change the session ends. Requests with an HTTP Basic header are + left to the HTTP Basic login as before. - Session removed in the settings: the session ends with the next request. A client with valid credentials signs in again and gets a new session. - OAuth2 and OpenID Connect: every request has to carry an Authorization @@ -46,6 +48,20 @@ an OAuth2 token are no longer accepted; the desktop and mobile clients send their token with every request. Sessions started before the update are taken over with their first request; if that request sends only the cookie and the session was not started with an app password, the client signs in once more. +Every check with an outdated password is logged as "Login failed" and counts +for the brute force protection. Behind a shared address (NAT), many such checks +after a password change can trigger the protection for all logins from that +address. + +Known limitation: a password change ends the sessions of clients that sign in +with the password as usual, but it does not reliably lock out someone who holds +a session (its cookie). A password session that got its session token with a +request carrying only the cookie has no password stored, and a password change +never ends it. After a lost device or leaked credentials, change the password +and also remove all other sessions under "Sessions", check and revoke the app +passwords and, if needed, disable the account for the time being. The +documentation describes this in the section "Gerät verloren oder Zugangsdaten +abgegriffen". The check reads the session token from the database as before, for app passwords also the app password token, which the HTTP Basic login of the same diff --git a/docs/administration/troubleshooting.md b/docs/administration/troubleshooting.md index 0ff3054e1d..edf00b2e58 100644 --- a/docs/administration/troubleshooting.md +++ b/docs/administration/troubleshooting.md @@ -110,20 +110,27 @@ vor jeder Anfrage: - **App-Passwort entzogen** (Persönlich → Sicherheit → App-Passwörter / Token): Die nächste Anfrage des Geräts bekommt 401, auch wenn es nur das Cookie schickt. Der Client muss mit einem neuen App-Passwort eingerichtet werden. -- **Kennwort geändert**: Sitzungen, die mit dem Kennwort begannen, enden - spätestens `last_check_timeout` Minuten (App-Wert `core`, Vorgabe 5) nach der - Änderung, auch wenn das Gerät nur das Cookie schickt. App-Passwörter, die vor - der Änderung entstanden, werden mit ihrer nächsten Prüfung ungültig (wie - bisher bei einer Anmeldung damit), ihre Sitzungen enden; der Client braucht - ein neues App-Passwort. Ausgenommen sind Konten aus LDAP: Deren App-Passwörter - prüft der Server nicht gegen das Kennwort, sie müssen entzogen werden. Jede +- **Kennwort geändert**: Ein Client, der sich mit dem Kennwort anmeldet, verliert + seine Sitzung spätestens `last_check_timeout` Minuten (App-Wert `core`, + Vorgabe 5) nach der Änderung: Schickt er das alte Kennwort mit, weist es die + Anmeldung mit HTTP-Basic ab; schickt er nur das Cookie, prüft der Server das + in der Sitzung hinterlegte Kennwort. Ist in der Sitzung kein Kennwort + hinterlegt, beendet ein Kennwortwechsel sie nicht. App-Passwörter, in denen + das alte Kennwort hinterlegt ist, werden mit ihrer nächsten Prüfung ungültig + (wie bisher bei einer Anmeldung damit), ihre Sitzungen enden; der Client + braucht ein neues App-Passwort. Gültig bleiben App-Passwörter ohne + hinterlegtes Kennwort, etwa solche, die in einer Sitzung ohne hinterlegtes + Kennwort angelegt wurden, und die App-Passwörter von Konten aus LDAP, die der + Server nicht gegen das Kennwort prüft: Sie müssen entzogen werden. Jede dieser Prüfungen mit dem alten Kennwort steht als „Login failed“ im Protokoll und zählt wie ein Fehlversuch für die Bremse gegen Passwort-Raten. + Ein Kennwortwechsel allein sperrt deshalb niemanden verlässlich aus, der eine + Sitzung hält (siehe + [Gerät verloren oder Zugangsdaten abgegriffen](#gerat-verloren-oder-zugangsdaten-abgegriffen)). - **Sitzung unter „Sitzungen“ entfernt**: Die Sitzung endet mit der nächsten Anfrage. Hat das Gerät noch gültige Zugangsdaten (App-Passwort, OAuth2-Token, Kennwort), meldet es sich damit sofort neu an. Um ein Gerät auszusperren, - dessen Zugang entziehen oder das Kennwort ändern, nicht nur die Sitzung - entfernen. + deshalb zusätzlich dessen Zugang entziehen, nicht nur die Sitzung entfernen. - **Desktop-Freigabe (OAuth2) widerrufen**: WebDAV- und OCS-Anfragen mit dem Token werden sofort abgewiesen. Die Sitzung lässt sich danach nicht mehr nutzen: Sie gilt nur zusammen mit einem Token, das die OAuth2-App für ihr @@ -146,6 +153,35 @@ angenommen; Desktop- und Mobil-Clients senden das Token immer mit. Sitzungen, die vor dem Update begannen und danach ohne App-Passwort nur das Cookie schicken, müssen sich einmal neu anmelden. +### Gerät verloren oder Zugangsdaten abgegriffen + +Ein Kennwortwechsel beendet die Sitzungen von Clients, die sich regulär mit dem +Kennwort anmelden, spätestens nach `last_check_timeout` Minuten. Wer eine +Sitzung hält, also ihr Cookie hat, ist damit aber nicht verlässlich +ausgesperrt. Wirksam sind dagegen das Entfernen der Sitzung unter „Sitzungen“ +und der Widerruf des App-Passworts, mit dem sie begann (die Sitzung endet mit +ihrer nächsten Anfrage), der Widerruf der OAuth2-Freigabe (die Sitzung lässt +sich danach nicht mehr nutzen) und, solange es deaktiviert ist, ein +deaktiviertes Konto. Bei Verdacht deshalb alles zusammen: + +1. Das Kennwort ändern. +2. Unter Persönlich → Sicherheit → „Sitzungen“ alle übrigen Sitzungen + entfernen. Dort steht eine Sitzung erst, sobald sie ein Sitzungstoken hat; + manche Clients bekommen es erst mit ihrer zweiten Anfrage. Die Liste + deshalb später noch einmal prüfen. +3. Die App-Passwörter prüfen und jedes entziehen, das nicht sicher zu einem + eigenen, weiter vertrauenswürdigen Gerät gehört, ebenso Desktop-Freigaben + (OAuth2) widerrufen. Betroffene Geräte neu einrichten. +4. Lässt sich das nicht sofort erledigen, das Konto bis dahin deaktivieren + (Benutzerverwaltung oder `occ user:disable `): Solange es deaktiviert + ist, lässt sich keine seiner Sitzungen nutzen. Das Deaktivieren + entfernt aber weder Sitzungen noch App-Passwörter; eine Sitzung, die + währenddessen keine Anfrage schickt, kann nach dem Wiedereinschalten weiter + gelten. Die Schritte 2 und 3 deshalb in jedem Fall nachholen. + +Öffentliche Links und Freigaben, die in dieser Zeit angelegt wurden, beendet +keiner dieser Schritte; sie gesondert prüfen. + ## JavaScript-Fehler: jQuery ist nicht definiert Ursache ist meist ein unvollständiger Build. Bei owncloud.online müssen Composer und `make` im Release-Prozess laufen.