diff options
136 files changed, 2450 insertions, 326 deletions
diff --git a/3rdparty b/3rdparty -Subproject 4921806dfb1c5c309eac60195ed34e2749baf3c +Subproject d80ec1fa2dad1c3ede272583e3c4f1f77f40141 diff --git a/apps/accessibility/l10n/fr.js b/apps/accessibility/l10n/fr.js index 20b104a8c35..a0b979adfb4 100644 --- a/apps/accessibility/l10n/fr.js +++ b/apps/accessibility/l10n/fr.js @@ -16,4 +16,4 @@ OC.L10N.register( "Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {guidelines}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "L'accès universel est très important pour nous. Nous suivons les standards du web et nous assurons que tout soit également utilisable sans souris et avec des logiciels d'assistance technique tels que les lecteurs d'écran. Nous visons à respecter les {guidelines}Règles pour l'accessibilité des contenus Web{linkend} 2.1 de niveau AA et même de niveau AAA avec le thème à fort contraste.", "If you find any issues, don’t hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!" : "Si vous rencontrez un problème, n'hésitez pas à nous le signaler sur {issuetracker}notre traqueur de problèmes{linkend}. Et si vous souhaitez vous impliquer, rejoignez {designteam}notre équipe de conception{linkend} !" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/accessibility/l10n/fr.json b/apps/accessibility/l10n/fr.json index 904b928d023..e684f3fde9f 100644 --- a/apps/accessibility/l10n/fr.json +++ b/apps/accessibility/l10n/fr.json @@ -13,5 +13,5 @@ "Provides multiple accessibilities options to ease your use of Nextcloud" : "Offre de multiples options d'accessibilité pour faciliter votre utilisation de Nextcloud", "Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {guidelines}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "L'accès universel est très important pour nous. Nous suivons les standards du web et nous assurons que tout soit également utilisable sans souris et avec des logiciels d'assistance technique tels que les lecteurs d'écran. Nous visons à respecter les {guidelines}Règles pour l'accessibilité des contenus Web{linkend} 2.1 de niveau AA et même de niveau AAA avec le thème à fort contraste.", "If you find any issues, don’t hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!" : "Si vous rencontrez un problème, n'hésitez pas à nous le signaler sur {issuetracker}notre traqueur de problèmes{linkend}. Et si vous souhaitez vous impliquer, rejoignez {designteam}notre équipe de conception{linkend} !" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/admin_audit/l10n/fr.js b/apps/admin_audit/l10n/fr.js index 31f8de41cf8..a69f15c0f47 100644 --- a/apps/admin_audit/l10n/fr.js +++ b/apps/admin_audit/l10n/fr.js @@ -4,4 +4,4 @@ OC.L10N.register( "Auditing / Logging" : "Audit / journalisation", "Provides logging abilities for Nextcloud such as logging file accesses or otherwise sensitive actions." : "Fournit des capacités de journalisation pour Nextcloud telles que l'enregistrement des accès aux fichiers ou d'autres actions sensibles." }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/admin_audit/l10n/fr.json b/apps/admin_audit/l10n/fr.json index 2cd134664f3..e52723ebc0f 100644 --- a/apps/admin_audit/l10n/fr.json +++ b/apps/admin_audit/l10n/fr.json @@ -1,5 +1,5 @@ { "translations": { "Auditing / Logging" : "Audit / journalisation", "Provides logging abilities for Nextcloud such as logging file accesses or otherwise sensitive actions." : "Fournit des capacités de journalisation pour Nextcloud telles que l'enregistrement des accès aux fichiers ou d'autres actions sensibles." -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/cloud_federation_api/l10n/fr.js b/apps/cloud_federation_api/l10n/fr.js index b4aaa45f0e0..19b39948c87 100644 --- a/apps/cloud_federation_api/l10n/fr.js +++ b/apps/cloud_federation_api/l10n/fr.js @@ -5,4 +5,4 @@ OC.L10N.register( "Enable clouds to communicate with each other and exchange data" : "Permettre aux clouds de communiquer entre eux et d'échanger des données", "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API Cloud Federation permet à diverses instances Nextcloud de communiquer entre elles et d'échanger des données." }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/cloud_federation_api/l10n/fr.json b/apps/cloud_federation_api/l10n/fr.json index 95816791116..b1936c0617e 100644 --- a/apps/cloud_federation_api/l10n/fr.json +++ b/apps/cloud_federation_api/l10n/fr.json @@ -2,5 +2,5 @@ "Cloud Federation API" : "API Cloud Federation", "Enable clouds to communicate with each other and exchange data" : "Permettre aux clouds de communiquer entre eux et d'échanger des données", "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API Cloud Federation permet à diverses instances Nextcloud de communiquer entre elles et d'échanger des données." -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/comments/l10n/fr.js b/apps/comments/l10n/fr.js index 662de67ace5..c1b71c1fea0 100644 --- a/apps/comments/l10n/fr.js +++ b/apps/comments/l10n/fr.js @@ -31,4 +31,4 @@ OC.L10N.register( "You were mentioned on “{file}”, in a comment by a user that has since been deleted" : "Vous avez été mentionné sur \"{file}\", dans un commentaire par un utilisateur qui a depuis été supprimé", "{user} mentioned you in a comment on “{file}”" : "{user} vous a mentionné⋅e dans un commentaire sur “{file}”" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/comments/l10n/fr.json b/apps/comments/l10n/fr.json index b201ded0b64..2a53eca9090 100644 --- a/apps/comments/l10n/fr.json +++ b/apps/comments/l10n/fr.json @@ -28,5 +28,5 @@ "An error occurred while trying to create the comment" : "Une erreur s'est produite lors de la tentative de création du commentaire", "You were mentioned on “{file}”, in a comment by a user that has since been deleted" : "Vous avez été mentionné sur \"{file}\", dans un commentaire par un utilisateur qui a depuis été supprimé", "{user} mentioned you in a comment on “{file}”" : "{user} vous a mentionné⋅e dans un commentaire sur “{file}”" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/contactsinteraction/l10n/fr.js b/apps/contactsinteraction/l10n/fr.js index d78f4b743ef..d0eb32facd0 100644 --- a/apps/contactsinteraction/l10n/fr.js +++ b/apps/contactsinteraction/l10n/fr.js @@ -6,4 +6,4 @@ OC.L10N.register( "Manages interaction between users and contacts" : "Gère l'interaction entre les utilisateurs et les contacts", "Collect data about user and contacts interactions and provide an address book for the data" : "Recueillir des données sur les interactions des utilisateurs et des contacts et fournir un carnet d'adresses pour les données" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/contactsinteraction/l10n/fr.json b/apps/contactsinteraction/l10n/fr.json index a22c1c385d3..23920f77906 100644 --- a/apps/contactsinteraction/l10n/fr.json +++ b/apps/contactsinteraction/l10n/fr.json @@ -3,5 +3,5 @@ "Contacts Interaction" : "Interaction des contacts", "Manages interaction between users and contacts" : "Gère l'interaction entre les utilisateurs et les contacts", "Collect data about user and contacts interactions and provide an address book for the data" : "Recueillir des données sur les interactions des utilisateurs et des contacts et fournir un carnet d'adresses pour les données" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/dashboard/l10n/fr.js b/apps/dashboard/l10n/fr.js index 8ea4ec17204..f7741c35506 100644 --- a/apps/dashboard/l10n/fr.js +++ b/apps/dashboard/l10n/fr.js @@ -29,4 +29,4 @@ OC.L10N.register( "Show something" : "Montre quelque chose", "Get more widgets from the app store" : "Obtenez plus de widgets de l'App Store" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/dashboard/l10n/fr.json b/apps/dashboard/l10n/fr.json index 3583484b90a..4fae965706b 100644 --- a/apps/dashboard/l10n/fr.json +++ b/apps/dashboard/l10n/fr.json @@ -26,5 +26,5 @@ "Insert from {productName}" : "Insérer depuis {productName}", "Show something" : "Montre quelque chose", "Get more widgets from the app store" : "Obtenez plus de widgets de l'App Store" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index cf02a3221fb..b797099dc0d 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -145,6 +145,7 @@ return array( 'OCA\\DAV\\Connector\\Sabre\\BlockLegacyClientPlugin' => $baseDir . '/../lib/Connector/Sabre/BlockLegacyClientPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\CachingTree' => $baseDir . '/../lib/Connector/Sabre/CachingTree.php', 'OCA\\DAV\\Connector\\Sabre\\ChecksumList' => $baseDir . '/../lib/Connector/Sabre/ChecksumList.php', + 'OCA\\DAV\\Connector\\Sabre\\ChecksumUpdatePlugin' => $baseDir . '/../lib/Connector/Sabre/ChecksumUpdatePlugin.php', 'OCA\\DAV\\Connector\\Sabre\\CommentPropertiesPlugin' => $baseDir . '/../lib/Connector/Sabre/CommentPropertiesPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\CopyEtagHeaderPlugin' => $baseDir . '/../lib/Connector/Sabre/CopyEtagHeaderPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\DavAclPlugin' => $baseDir . '/../lib/Connector/Sabre/DavAclPlugin.php', @@ -272,6 +273,7 @@ return array( 'OCA\\DAV\\Migration\\Version1016Date20201109085907' => $baseDir . '/../lib/Migration/Version1016Date20201109085907.php', 'OCA\\DAV\\Migration\\Version1017Date20210216083742' => $baseDir . '/../lib/Migration/Version1017Date20210216083742.php', 'OCA\\DAV\\Migration\\Version1018Date20210312100735' => $baseDir . '/../lib/Migration/Version1018Date20210312100735.php', + 'OCA\\DAV\\Profiler\\ProfilerPlugin' => $baseDir . '/../lib/Profiler/ProfilerPlugin.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', 'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index d164ab2b1ce..930b531b335 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -160,6 +160,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Connector\\Sabre\\BlockLegacyClientPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/BlockLegacyClientPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\CachingTree' => __DIR__ . '/..' . '/../lib/Connector/Sabre/CachingTree.php', 'OCA\\DAV\\Connector\\Sabre\\ChecksumList' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ChecksumList.php', + 'OCA\\DAV\\Connector\\Sabre\\ChecksumUpdatePlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ChecksumUpdatePlugin.php', 'OCA\\DAV\\Connector\\Sabre\\CommentPropertiesPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/CommentPropertiesPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\CopyEtagHeaderPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/CopyEtagHeaderPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\DavAclPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/DavAclPlugin.php', @@ -287,6 +288,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Migration\\Version1016Date20201109085907' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date20201109085907.php', 'OCA\\DAV\\Migration\\Version1017Date20210216083742' => __DIR__ . '/..' . '/../lib/Migration/Version1017Date20210216083742.php', 'OCA\\DAV\\Migration\\Version1018Date20210312100735' => __DIR__ . '/..' . '/../lib/Migration/Version1018Date20210312100735.php', + 'OCA\\DAV\\Profiler\\ProfilerPlugin' => __DIR__ . '/..' . '/../lib/Profiler/ProfilerPlugin.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', 'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php', diff --git a/apps/dav/l10n/fr.js b/apps/dav/l10n/fr.js index 1a614722d48..52d9a58837b 100644 --- a/apps/dav/l10n/fr.js +++ b/apps/dav/l10n/fr.js @@ -173,4 +173,4 @@ OC.L10N.register( "Your attendance was updated successfully." : "Votre présence a été mise à jour avec succès.", "Calendar and tasks" : "Agenda et tâches" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/dav/l10n/fr.json b/apps/dav/l10n/fr.json index f728c2d1b3b..f11499dbefd 100644 --- a/apps/dav/l10n/fr.json +++ b/apps/dav/l10n/fr.json @@ -170,5 +170,5 @@ "Comment" : "Commentaire", "Your attendance was updated successfully." : "Votre présence a été mise à jour avec succès.", "Calendar and tasks" : "Agenda et tâches" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/dav/l10n/pt_BR.js b/apps/dav/l10n/pt_BR.js index abdf7451380..bd1c369268d 100644 --- a/apps/dav/l10n/pt_BR.js +++ b/apps/dav/l10n/pt_BR.js @@ -113,6 +113,18 @@ OC.L10N.register( "Could not write file contents" : "Não foi possível gravar o conteúdo do arquivo", "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"], "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Erro ao copiar o arquivo para o local de destino (copiado: %1$s, tamanho de arquivo esperado: %2$s)", + "Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Tamanho de arquivo esperado de %1$s mas lido (do cliente Nextcloud) e gravado (no armazenamento Nextcloud) %2$s. Pode ser um problema de rede no lado de envio ou um problema de gravação no armazenamento no lado do servidor.", + "Could not rename part file to final file, canceled by hook" : "Não foi possível renomear o arquivo de parte para o arquivo final, cancelado pelo gancho", + "Could not rename part file to final file" : "Não foi possível renomear o arquivo de parte para o arquivo final", + "Failed to check file size: %1$s" : "Falha ao verificar o tamanho do arquivo: %1$s", + "Could not open file" : "Não pode abrir o arquivo", + "Encryption not ready: %1$s" : "A criptografia não está pronta: %1$s", + "Failed to open file: %1$s" : "Falha ao abrir arquivo: %1$s", + "Failed to unlink: %1$s" : "Falha ao desvincular: %1$s", + "Invalid chunk name" : "Nome do bloco inválido", + "Could not rename part file assembled from chunks" : "Não foi possível renomear parte do arquivo montado a partir de pedaços", + "Failed to write file contents: %1$s" : "Falha ao gravar o conteúdo do arquivo:%1$s", + "File not found: %1$s" : "Arquivo não encontrado:%1$s", "System is in maintenance mode." : "O sistema está em modo de manutenção", "Upgrade needed" : "Upgrade necessário", "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Seu %s precisa estar configurado para usar HTTPS a fim de usar o CalDAV e o CardDAV com o iOS/macOS.", diff --git a/apps/dav/l10n/pt_BR.json b/apps/dav/l10n/pt_BR.json index 26c62d6bed8..ffa2f55511e 100644 --- a/apps/dav/l10n/pt_BR.json +++ b/apps/dav/l10n/pt_BR.json @@ -111,6 +111,18 @@ "Could not write file contents" : "Não foi possível gravar o conteúdo do arquivo", "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"], "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Erro ao copiar o arquivo para o local de destino (copiado: %1$s, tamanho de arquivo esperado: %2$s)", + "Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Tamanho de arquivo esperado de %1$s mas lido (do cliente Nextcloud) e gravado (no armazenamento Nextcloud) %2$s. Pode ser um problema de rede no lado de envio ou um problema de gravação no armazenamento no lado do servidor.", + "Could not rename part file to final file, canceled by hook" : "Não foi possível renomear o arquivo de parte para o arquivo final, cancelado pelo gancho", + "Could not rename part file to final file" : "Não foi possível renomear o arquivo de parte para o arquivo final", + "Failed to check file size: %1$s" : "Falha ao verificar o tamanho do arquivo: %1$s", + "Could not open file" : "Não pode abrir o arquivo", + "Encryption not ready: %1$s" : "A criptografia não está pronta: %1$s", + "Failed to open file: %1$s" : "Falha ao abrir arquivo: %1$s", + "Failed to unlink: %1$s" : "Falha ao desvincular: %1$s", + "Invalid chunk name" : "Nome do bloco inválido", + "Could not rename part file assembled from chunks" : "Não foi possível renomear parte do arquivo montado a partir de pedaços", + "Failed to write file contents: %1$s" : "Falha ao gravar o conteúdo do arquivo:%1$s", + "File not found: %1$s" : "Arquivo não encontrado:%1$s", "System is in maintenance mode." : "O sistema está em modo de manutenção", "Upgrade needed" : "Upgrade necessário", "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Seu %s precisa estar configurado para usar HTTPS a fim de usar o CalDAV e o CardDAV com o iOS/macOS.", diff --git a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php new file mode 100644 index 00000000000..3247259357f --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php @@ -0,0 +1,83 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class ChecksumUpdatePlugin extends ServerPlugin { + /** + * @var \Sabre\DAV\Server + */ + protected $server; + + public function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + $server->on('method:PATCH', [$this, 'httpPatch']); + } + + public function getPluginName(): string { + return 'checksumupdate'; + } + + public function getHTTPMethods($path): array { + $tree = $this->server->tree; + + if ($tree->nodeExists($path)) { + $node = $tree->getNodeForPath($path); + if ($node instanceof File) { + return ['PATCH']; + } + } + + return []; + } + + public function getFeatures(): array { + return ['nextcloud-checksum-update']; + } + + public function httpPatch(RequestInterface $request, ResponseInterface $response) { + $path = $request->getPath(); + + $node = $this->server->tree->getNodeForPath($path); + if ($node instanceof File) { + $type = strtolower( + (string)$request->getHeader('X-Recalculate-Hash') + ); + + $hash = $node->hash($type); + if ($hash) { + $checksum = strtoupper($type) . ':' . $hash; + $node->setChecksum($checksum); + $response->addHeader('OC-Checksum', $checksum); + $response->setHeader('Content-Length', '0'); + $response->setStatus(204); + + return false; + } + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index 3ae4416d363..bd92b3b22a4 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -327,8 +327,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol return $this->quotaInfo; } try { - $info = $this->fileView->getFileInfo($this->path, false); - $storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $info); + $storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $this->info, false); if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) { $free = \OCP\Files\FileInfo::SPACE_UNLIMITED; } else { diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index 4768fc3dc44..70dffbaaaed 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -388,11 +388,9 @@ class File extends Node implements IFile { if (isset($this->request->server['HTTP_OC_CHECKSUM'])) { $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']); - $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]); - $this->refreshInfo(); + $this->setChecksum($checksum); } elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') { - $this->fileView->putFileInfo($this->path, ['checksum' => '']); - $this->refreshInfo(); + $this->setChecksum(''); } } catch (StorageNotAvailableException $e) { throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e); @@ -741,9 +739,18 @@ class File extends Node implements IFile { return $this->info->getChecksum(); } + public function setChecksum(string $checksum) { + $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]); + $this->refreshInfo(); + } + protected function header($string) { if (!\OC::$CLI) { \header($string); } } + + public function hash(string $type) { + return $this->fileView->hash($type, $this->path); + } } diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index 095fb631c2b..b13dbd20ca9 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -180,6 +180,7 @@ class ServerFactory { ) ); $server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view, true)); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\ChecksumUpdatePlugin()); if ($this->userSession->isLoggedIn()) { $server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager)); diff --git a/apps/dav/lib/Profiler/ProfilerPlugin.php b/apps/dav/lib/Profiler/ProfilerPlugin.php new file mode 100644 index 00000000000..672ca4010b7 --- /dev/null +++ b/apps/dav/lib/Profiler/ProfilerPlugin.php @@ -0,0 +1,47 @@ +<?php declare(strict_types = 1); +/** + * @copyright 2021 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\Profiler; + +use OCP\IRequest; +use Sabre\DAV\Server; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class ProfilerPlugin extends \Sabre\DAV\ServerPlugin { + private IRequest $request; + + public function __construct(IRequest $request) { + $this->request = $request; + } + + /** @return void */ + public function initialize(Server $server) { + $server->on('afterMethod:*', [$this, 'afterMethod']); + } + + /** @return void */ + public function afterMethod(RequestInterface $request, ResponseInterface $response) { + $response->addHeader('X-Debug-Token', $this->request->getId()); + } +} diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 759d39c0233..589e6c2bd6c 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -36,6 +36,9 @@ namespace OCA\DAV; use OCA\DAV\Connector\Sabre\RequestIdHeaderPlugin; use OCP\Diagnostics\IEventLogger; +use OCP\Profiler\IProfiler; +use OCA\DAV\Profiler\ProfilerPlugin; +use OCP\AppFramework\Http\Response; use Psr\Log\LoggerInterface; use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\CalDAV\BirthdayService; @@ -49,6 +52,7 @@ use OCA\DAV\Connector\Sabre\Auth; use OCA\DAV\Connector\Sabre\BearerAuth; use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; use OCA\DAV\Connector\Sabre\CachingTree; +use OCA\DAV\Connector\Sabre\ChecksumUpdatePlugin; use OCA\DAV\Connector\Sabre\CommentPropertiesPlugin; use OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin; use OCA\DAV\Connector\Sabre\DavAclPlugin; @@ -78,17 +82,19 @@ use Sabre\DAV\UUIDUtil; use SearchDAV\DAV\SearchPlugin; class Server { + private IRequest $request; + private string $baseUri; + public Connector\Sabre\Server $server; + private IProfiler $profiler; + + public function __construct(IRequest $request, string $baseUri) { + $this->profiler = \OC::$server->get(IProfiler::class); + if ($this->profiler->isEnabled()) { + /** @var IEventLogger $eventLogger */ + $eventLogger = \OC::$server->get(IEventLogger::class); + $eventLogger->start('runtime', 'DAV Runtime'); + } - /** @var IRequest */ - private $request; - - /** @var string */ - private $baseUri; - - /** @var Connector\Sabre\Server */ - public $server; - - public function __construct(IRequest $request, $baseUri) { $this->request = $request; $this->baseUri = $baseUri; $logger = \OC::$server->getLogger(); @@ -115,6 +121,7 @@ class Server { $this->server->httpRequest->setUrl($this->request->getRequestUri()); $this->server->setBaseUri($this->baseUri); + $this->server->addPlugin(new ProfilerPlugin($this->request)); $this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig())); $this->server->addPlugin(new AnonymousOptionsPlugin()); $authPlugin = new Plugin(); @@ -247,6 +254,7 @@ class Server { !\OC::$server->getConfig()->getSystemValue('debug', false) ) ); + $this->server->addPlugin(new ChecksumUpdatePlugin()); $this->server->addPlugin( new \Sabre\DAV\PropertyStorage\Plugin( @@ -343,6 +351,11 @@ class Server { $eventLogger->start('dav_server_exec', ''); $this->server->exec(); $eventLogger->end('dav_server_exec'); + if ($this->profiler->isEnabled()) { + $eventLogger->end('runtime'); + $profile = $this->profiler->collect(\OC::$server->get(IRequest::class), new Response()); + $this->profiler->saveProfile($profile); + } } private function requestIsForSubtree(array $subTrees): bool { diff --git a/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php index 024a6432d01..c88d2302bec 100644 --- a/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php @@ -299,10 +299,6 @@ class DirectoryTest extends \Test\TestCase { ->method('getMountPoint') ->willReturn($mountPoint); - $this->view->expects($this->once()) - ->method('getFileInfo') - ->willReturn($this->info); - $mountPoint->method('getMountPoint') ->willReturn('/user/files/mymountpoint'); @@ -344,10 +340,6 @@ class DirectoryTest extends \Test\TestCase { $mountPoint->method('getMountPoint') ->willReturn('/user/files/mymountpoint'); - $this->view->expects($this->once()) - ->method('getFileInfo') - ->willReturn($this->info); - $dir = new Directory($this->view, $this->info); $this->assertEquals([200, 800], $dir->getQuotaInfo()); //200 used, 800 free } diff --git a/apps/encryption/l10n/fr.js b/apps/encryption/l10n/fr.js index 3e943717528..ecc2e37b13f 100644 --- a/apps/encryption/l10n/fr.js +++ b/apps/encryption/l10n/fr.js @@ -65,4 +65,4 @@ OC.L10N.register( "Hey there,<br><br>the admin enabled server-side-encryption. Your files were encrypted using the password <strong>%s</strong>.<br><br>Please login to the web interface, go to the section \"basic encryption module\" of your personal settings and update your encryption password by entering this password into the \"old log-in password\" field and your current login-password.<br><br>" : "Bonjour,\n<br><br>\nL'administrateur a activé le chiffrement sur le serveur. Vos fichiers ont été chiffrés avec le mot de passe suivant :\n\n<p style=\"font-family: monospace;\"><b>%s</b></p>\n\n<p>\nVeuillez suivre ces instructions :\n<ol>\n<li>Connectez-vous à l'interface web et trouvez la section <em>\"Module de chiffrement de base d'\"</em> dans vos paramètres personnels;</li>\n<li>Entrez le mot de passe fourni ci-dessus dans le champ <em>\"Ancien mot de passe de connexion\"</em>;</li>\n<li>Entrez le mot de passe que vous utilisez actuellement pour vous connecter dans le champ <em>\"Actuel mot de passe de connexion\"</em>;</li>\n<li>Validez en cliquant sur le bouton <em>\"Mettre à jour le mot de passe de votre clef privée\"</em>.</li>\n</ol>\n</p>", "Hey there,\n\nthe admin enabled server-side-encryption. Your files were encrypted using the password \"%s\".\n\nPlease login to the web interface, go to the section \"basic encryption module\" of your personal settings and update your encryption password by entering this password into the \"old log-in password\" field and your current login-password.\n\n" : "Bonjour,\n\nL'administrateur a activé le chiffrement côté serveur. Vos fichiers ont été chiffrés avec le mot de passe \"%s\".\n\nVeuillez vous connecter à l'interface web, vous rendre à la section \"Module de chiffrement de base\" de vos paramètres personnels et mettre à jour votre mot de passe de chiffrement en insérant ce mot de passe dans le champ \"Ancien mot de passe de connexion\" ainsi que votre mot de passe actuel.\n\n" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/encryption/l10n/fr.json b/apps/encryption/l10n/fr.json index 3928cae76e8..06c8550db74 100644 --- a/apps/encryption/l10n/fr.json +++ b/apps/encryption/l10n/fr.json @@ -62,5 +62,5 @@ "Hey there,\n\nthe admin enabled server-side-encryption. Your files were encrypted using the password '%s'.\n\nPlease login to the web interface, go to the section 'basic encryption module' of your personal settings and update your encryption password by entering this password into the 'old log-in password' field and your current login-password.\n\n" : "Bonjour,\n\nL'administrateur a activé le chiffrement sur le serveur. Vos fichiers ont été chiffrés avec le mot de passe suivant :\n\n%s\n\nVeuillez suivre ces instructions :\n\n1. Connectez-vous à l'interface web et trouvez la section \"Module de chiffrement de base d'\" dans vos paramètres personnels;\n\n2. Entrez le mot de passe fourni ci-dessus dans le champ \"Ancien mot de passe de connexion\";\n\n3. Entrez le mot de passe que vous utilisez actuellement pour vous connecter dans le champ \"Actuel mot de passe de connexion\";\n\n4. Validez en cliquant sur le bouton \"Mettre à jour le mot de passe de votre clef privée\".\n", "Hey there,<br><br>the admin enabled server-side-encryption. Your files were encrypted using the password <strong>%s</strong>.<br><br>Please login to the web interface, go to the section \"basic encryption module\" of your personal settings and update your encryption password by entering this password into the \"old log-in password\" field and your current login-password.<br><br>" : "Bonjour,\n<br><br>\nL'administrateur a activé le chiffrement sur le serveur. Vos fichiers ont été chiffrés avec le mot de passe suivant :\n\n<p style=\"font-family: monospace;\"><b>%s</b></p>\n\n<p>\nVeuillez suivre ces instructions :\n<ol>\n<li>Connectez-vous à l'interface web et trouvez la section <em>\"Module de chiffrement de base d'\"</em> dans vos paramètres personnels;</li>\n<li>Entrez le mot de passe fourni ci-dessus dans le champ <em>\"Ancien mot de passe de connexion\"</em>;</li>\n<li>Entrez le mot de passe que vous utilisez actuellement pour vous connecter dans le champ <em>\"Actuel mot de passe de connexion\"</em>;</li>\n<li>Validez en cliquant sur le bouton <em>\"Mettre à jour le mot de passe de votre clef privée\"</em>.</li>\n</ol>\n</p>", "Hey there,\n\nthe admin enabled server-side-encryption. Your files were encrypted using the password \"%s\".\n\nPlease login to the web interface, go to the section \"basic encryption module\" of your personal settings and update your encryption password by entering this password into the \"old log-in password\" field and your current login-password.\n\n" : "Bonjour,\n\nL'administrateur a activé le chiffrement côté serveur. Vos fichiers ont été chiffrés avec le mot de passe \"%s\".\n\nVeuillez vous connecter à l'interface web, vous rendre à la section \"Module de chiffrement de base\" de vos paramètres personnels et mettre à jour votre mot de passe de chiffrement en insérant ce mot de passe dans le champ \"Ancien mot de passe de connexion\" ainsi que votre mot de passe actuel.\n\n" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/federatedfilesharing/l10n/fr.js b/apps/federatedfilesharing/l10n/fr.js index c7d247ed4b3..b36a05a2be8 100644 --- a/apps/federatedfilesharing/l10n/fr.js +++ b/apps/federatedfilesharing/l10n/fr.js @@ -52,4 +52,4 @@ OC.L10N.register( "HTML Code:" : "Code HTML :", "Adjust how people can share between servers." : "Réglez comment les personnes peuvent partager entre les serveurs." }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/federatedfilesharing/l10n/fr.json b/apps/federatedfilesharing/l10n/fr.json index 33c510fffc6..bda2d626ada 100644 --- a/apps/federatedfilesharing/l10n/fr.json +++ b/apps/federatedfilesharing/l10n/fr.json @@ -49,5 +49,5 @@ "Share with me via Nextcloud" : "Partagez avec moi via Nextcloud", "HTML Code:" : "Code HTML :", "Adjust how people can share between servers." : "Réglez comment les personnes peuvent partager entre les serveurs." -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/fr.js b/apps/federation/l10n/fr.js index 365a1dbc273..212928d43b1 100644 --- a/apps/federation/l10n/fr.js +++ b/apps/federation/l10n/fr.js @@ -14,4 +14,4 @@ OC.L10N.register( "Trusted server" : "Serveur de confiance", "Add" : "Ajouter" }, -"nplurals=2; plural=(n > 1);"); +"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 c100c46ee86..ca5471977bf 100644 --- a/apps/federation/l10n/fr.json +++ b/apps/federation/l10n/fr.json @@ -11,5 +11,5 @@ "+ Add trusted server" : "+ Ajouter un serveur de confiance", "Trusted server" : "Serveur de confiance", "Add" : "Ajouter" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/files/l10n/fr.js b/apps/files/l10n/fr.js index 34fb95fc5f4..6a1fe9634c7 100644 --- a/apps/files/l10n/fr.js +++ b/apps/files/l10n/fr.js @@ -217,4 +217,4 @@ OC.L10N.register( "Your storage is full, files can not be updated or synced anymore!" : "Votre espace de stockage est plein. Les fichiers ne peuvent plus être mis à jour ni synchronisés !", "_matches '{filter}'_::_match '{filter}'_" : ["correspond à '{filter}'","correspondent à '{filter}'"] }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/files/l10n/fr.json b/apps/files/l10n/fr.json index 4954a479c53..f3325029bfc 100644 --- a/apps/files/l10n/fr.json +++ b/apps/files/l10n/fr.json @@ -214,5 +214,5 @@ "External storage \"{mountPoint}\" is full, files can not be updated or synced anymore!" : "L'espace de stockage externe \"{mountPoint}\" est plein, les fichiers ne peuvent plus être mis à jour ni synchronisés !", "Your storage is full, files can not be updated or synced anymore!" : "Votre espace de stockage est plein. Les fichiers ne peuvent plus être mis à jour ni synchronisés !", "_matches '{filter}'_::_match '{filter}'_" : ["correspond à '{filter}'","correspondent à '{filter}'"] -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/files_external/l10n/fr.js b/apps/files_external/l10n/fr.js index a50d3511709..a82a0e90e86 100644 --- a/apps/files_external/l10n/fr.js +++ b/apps/files_external/l10n/fr.js @@ -139,4 +139,4 @@ OC.L10N.register( "SMB / CIFS" : "SMB / CIFS", "SMB / CIFS using OC login" : "SMB / CIFS en utilisant les identifiants OC" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/files_external/l10n/fr.json b/apps/files_external/l10n/fr.json index 5924e6c9eba..c9ca07e930d 100644 --- a/apps/files_external/l10n/fr.json +++ b/apps/files_external/l10n/fr.json @@ -136,5 +136,5 @@ "(group)" : "(groupe)", "SMB / CIFS" : "SMB / CIFS", "SMB / CIFS using OC login" : "SMB / CIFS en utilisant les identifiants OC" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/files_sharing/l10n/fr.js b/apps/files_sharing/l10n/fr.js index ff4480866e0..6a5e9aced0e 100644 --- a/apps/files_sharing/l10n/fr.js +++ b/apps/files_sharing/l10n/fr.js @@ -255,4 +255,4 @@ OC.L10N.register( "Download %s" : "Télécharger %s", "Cannot change permissions for public share links" : "Impossible de changer les autorisations pour les liens publics partagés" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/files_sharing/l10n/fr.json b/apps/files_sharing/l10n/fr.json index bec5504c88d..8fafa4151d8 100644 --- a/apps/files_sharing/l10n/fr.json +++ b/apps/files_sharing/l10n/fr.json @@ -252,5 +252,5 @@ "Sharing sending the password by Nextcloud Talk failed because Nextcloud Talk is not enabled" : "Le partage de l'envoi du mot de passe par Nextcloud Talk a échoué parce que Nextcloud Talk n'est pas activé.", "Download %s" : "Télécharger %s", "Cannot change permissions for public share links" : "Impossible de changer les autorisations pour les liens publics partagés" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/files_sharing/l10n/hu.js b/apps/files_sharing/l10n/hu.js index 68058b83832..d644dfe78db 100644 --- a/apps/files_sharing/l10n/hu.js +++ b/apps/files_sharing/l10n/hu.js @@ -124,6 +124,7 @@ OC.L10N.register( "Could not lock path" : "Nem sikerült zárolni az útvonalat", "Wrong or no update parameter given" : "Hibás vagy üres frissítési paraméter", "Share must at least have READ or CREATE permissions" : "A megosztásnak legalább OLVASÁSI és LÉTREHOZÁSI engedéllyel kell rendelkeznie", + "Share must have READ permission if UPDATE or DELETE permission is set" : "A megosztásnak OLVASÁSI jogosultsággal kell rendelkeznie, ha a FRISSÍTÉSI vagy TÖRLÉSI jogosultság meg van adva", "\"Sending the password by Nextcloud Talk\" for sharing a file or folder failed because Nextcloud Talk is not enabled." : "A „Jelszó kiküldése a Nextcloud Beszélgetéssel” nem sikerült a fájlnál vagy mappánál, mert a Nextcloud Beszélgetés nem engedélyezett.", "shared by %s" : "megosztotta: %s", "Download all files" : "Összes fájl letöltése", diff --git a/apps/files_sharing/l10n/hu.json b/apps/files_sharing/l10n/hu.json index fa7077dfbde..1a8da9691e9 100644 --- a/apps/files_sharing/l10n/hu.json +++ b/apps/files_sharing/l10n/hu.json @@ -122,6 +122,7 @@ "Could not lock path" : "Nem sikerült zárolni az útvonalat", "Wrong or no update parameter given" : "Hibás vagy üres frissítési paraméter", "Share must at least have READ or CREATE permissions" : "A megosztásnak legalább OLVASÁSI és LÉTREHOZÁSI engedéllyel kell rendelkeznie", + "Share must have READ permission if UPDATE or DELETE permission is set" : "A megosztásnak OLVASÁSI jogosultsággal kell rendelkeznie, ha a FRISSÍTÉSI vagy TÖRLÉSI jogosultság meg van adva", "\"Sending the password by Nextcloud Talk\" for sharing a file or folder failed because Nextcloud Talk is not enabled." : "A „Jelszó kiküldése a Nextcloud Beszélgetéssel” nem sikerült a fájlnál vagy mappánál, mert a Nextcloud Beszélgetés nem engedélyezett.", "shared by %s" : "megosztotta: %s", "Download all files" : "Összes fájl letöltése", diff --git a/apps/files_trashbin/l10n/fr.js b/apps/files_trashbin/l10n/fr.js index 23f5d61a3e1..25e1eea585f 100644 --- a/apps/files_trashbin/l10n/fr.js +++ b/apps/files_trashbin/l10n/fr.js @@ -28,4 +28,4 @@ OC.L10N.register( "Error while emptying trashbin" : "Erreur lors du vidage de la corbeille", "Error while removing files from trashbin" : "Erreur lors de la suppression des fichiers de la corbeille" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/files_trashbin/l10n/fr.json b/apps/files_trashbin/l10n/fr.json index 04f4c1dcdad..21b04506c4d 100644 --- a/apps/files_trashbin/l10n/fr.json +++ b/apps/files_trashbin/l10n/fr.json @@ -25,5 +25,5 @@ "Error while restoring files from trashbin" : "Erreur lors de la restauration des fichiers de la corbeille", "Error while emptying trashbin" : "Erreur lors du vidage de la corbeille", "Error while removing files from trashbin" : "Erreur lors de la suppression des fichiers de la corbeille" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/files_versions/l10n/fr.js b/apps/files_versions/l10n/fr.js index 4f42ff26e91..ad4d6b7e15b 100644 --- a/apps/files_versions/l10n/fr.js +++ b/apps/files_versions/l10n/fr.js @@ -9,4 +9,4 @@ OC.L10N.register( "Restore" : "Restaurer", "No other versions available" : "Aucune autre version n'est disponible" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/files_versions/l10n/fr.json b/apps/files_versions/l10n/fr.json index aa138df6cc4..097a057d305 100644 --- a/apps/files_versions/l10n/fr.json +++ b/apps/files_versions/l10n/fr.json @@ -6,5 +6,5 @@ "_%n byte_::_%n bytes_" : ["%n octet","%n octets"], "Restore" : "Restaurer", "No other versions available" : "Aucune autre version n'est disponible" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/lookup_server_connector/l10n/fr.js b/apps/lookup_server_connector/l10n/fr.js index 87fc8406884..1adb735ff96 100644 --- a/apps/lookup_server_connector/l10n/fr.js +++ b/apps/lookup_server_connector/l10n/fr.js @@ -4,4 +4,4 @@ OC.L10N.register( "Lookup Server Connector" : "Connecteur de serveur de recherche", "Sync public user information with the lookup server" : "Synchroniser les informations utilisateur publiques avec le serveur de recherche" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/lookup_server_connector/l10n/fr.json b/apps/lookup_server_connector/l10n/fr.json index 346d7bb5766..8554a0baee9 100644 --- a/apps/lookup_server_connector/l10n/fr.json +++ b/apps/lookup_server_connector/l10n/fr.json @@ -1,5 +1,5 @@ { "translations": { "Lookup Server Connector" : "Connecteur de serveur de recherche", "Sync public user information with the lookup server" : "Synchroniser les informations utilisateur publiques avec le serveur de recherche" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/oauth2/l10n/fr.js b/apps/oauth2/l10n/fr.js index c8080f0319d..e334cc79b87 100644 --- a/apps/oauth2/l10n/fr.js +++ b/apps/oauth2/l10n/fr.js @@ -17,4 +17,4 @@ OC.L10N.register( "Show client secret" : "Afficher client secret", "Delete" : "Supprimer" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/oauth2/l10n/fr.json b/apps/oauth2/l10n/fr.json index 9f079bd9ad2..9c47f7a24d2 100644 --- a/apps/oauth2/l10n/fr.json +++ b/apps/oauth2/l10n/fr.json @@ -14,5 +14,5 @@ "Secret" : "Secret", "Show client secret" : "Afficher client secret", "Delete" : "Supprimer" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/provisioning_api/l10n/fr.js b/apps/provisioning_api/l10n/fr.js index 6f6bf89f040..3470575d390 100644 --- a/apps/provisioning_api/l10n/fr.js +++ b/apps/provisioning_api/l10n/fr.js @@ -15,4 +15,4 @@ OC.L10N.register( "This application enables a set of APIs that external systems can use to create, edit, delete and query user\n\t\tattributes, query, set and remove groups, set quota and query total storage used in Nextcloud. Group admin users\n\t\tcan also query Nextcloud and perform the same functions as an admin for groups they manage. The API also enables\n\t\tan admin to query for active Nextcloud applications, application info, and to enable or disable an app remotely.\n\t\tOnce the app is enabled, HTTP requests can be used via a Basic Auth header to perform any of the functions\n\t\tlisted above. More information is available in the Provisioning API documentation, including example calls\n\t\tand server responses." : "Cette application active un ensemble d'API qui peuvent être utilisées par un système externe pour créer, modifier, supprimer et rechercher des attributs d'utilisateur, rechercher, ajouter et retirer des groupes, fixer des quotas et rechercher l'espace de stockage total utilisé sur Nextcloud. Les administrateurs de groupe peuvent aussi rechercher Nextcloud et accéder aux même fonctionnalités que les administrateurs pour les groupes dont ils ont la gestion. L'API permet aussi à un administrateur de rechercher les applications Nextcloud actives et les informations d'application ainsi que d'activer et désactiver les applications à distance. Une fois l'application activée, des requêtes HTTP peuvent être utilisées au moyen d'un entête Basic Auth pour exécuter chacune des fonctionnalités listées ci-dessus. Des informations supplémentaires sont accessibles dans la documentation sur l'API de provisionnement, avec des exemples de demandes et réponses serveur.", "An unexpected error occurred. Please consult your sysadmin." : "Une erreur inattendue est survenue. Veuillez contacter votre administrateur système." }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/provisioning_api/l10n/fr.json b/apps/provisioning_api/l10n/fr.json index 440a83f4168..206f3571917 100644 --- a/apps/provisioning_api/l10n/fr.json +++ b/apps/provisioning_api/l10n/fr.json @@ -12,5 +12,5 @@ "This application enables a set of APIs that external systems can use to manage users, groups and apps." : "Cette application active un ensemble d'API que les systèmes externes peuvent utiliser pour gérer les utilisateurs, les groupes et les applications.", "This application enables a set of APIs that external systems can use to create, edit, delete and query user\n\t\tattributes, query, set and remove groups, set quota and query total storage used in Nextcloud. Group admin users\n\t\tcan also query Nextcloud and perform the same functions as an admin for groups they manage. The API also enables\n\t\tan admin to query for active Nextcloud applications, application info, and to enable or disable an app remotely.\n\t\tOnce the app is enabled, HTTP requests can be used via a Basic Auth header to perform any of the functions\n\t\tlisted above. More information is available in the Provisioning API documentation, including example calls\n\t\tand server responses." : "Cette application active un ensemble d'API qui peuvent être utilisées par un système externe pour créer, modifier, supprimer et rechercher des attributs d'utilisateur, rechercher, ajouter et retirer des groupes, fixer des quotas et rechercher l'espace de stockage total utilisé sur Nextcloud. Les administrateurs de groupe peuvent aussi rechercher Nextcloud et accéder aux même fonctionnalités que les administrateurs pour les groupes dont ils ont la gestion. L'API permet aussi à un administrateur de rechercher les applications Nextcloud actives et les informations d'application ainsi que d'activer et désactiver les applications à distance. Une fois l'application activée, des requêtes HTTP peuvent être utilisées au moyen d'un entête Basic Auth pour exécuter chacune des fonctionnalités listées ci-dessus. Des informations supplémentaires sont accessibles dans la documentation sur l'API de provisionnement, avec des exemples de demandes et réponses serveur.", "An unexpected error occurred. Please consult your sysadmin." : "Une erreur inattendue est survenue. Veuillez contacter votre administrateur système." -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/settings/l10n/fr.js b/apps/settings/l10n/fr.js index 96cc598eb43..5ac44bbd9af 100644 --- a/apps/settings/l10n/fr.js +++ b/apps/settings/l10n/fr.js @@ -510,4 +510,4 @@ OC.L10N.register( "For password reset and notifications" : "Pour la réinitialisation du mot de passe et les notifications", "Set as primary mail" : "Définir comme e-mail principal" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/settings/l10n/fr.json b/apps/settings/l10n/fr.json index 4f3b4f08f68..4a108185728 100644 --- a/apps/settings/l10n/fr.json +++ b/apps/settings/l10n/fr.json @@ -507,5 +507,5 @@ "Change privacy level of email" : "Changer le niveau de confidentialité de l'e-mail", "For password reset and notifications" : "Pour la réinitialisation du mot de passe et les notifications", "Set as primary mail" : "Définir comme e-mail principal" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/sharebymail/l10n/fr.js b/apps/sharebymail/l10n/fr.js index 5a8b687a6c0..51d0fa944a3 100644 --- a/apps/sharebymail/l10n/fr.js +++ b/apps/sharebymail/l10n/fr.js @@ -54,4 +54,4 @@ OC.L10N.register( "We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again." : "Nous ne pouvons pas vous envoyer le mot de passe généré automatiquement. Veuillez renseigner une adresse e-mail valide dans vos paramètres personnels puis réessayer.", "Enforce password protection" : "Imposer la protection par mot de passe" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/sharebymail/l10n/fr.json b/apps/sharebymail/l10n/fr.json index c2666e70f09..ae72df39da1 100644 --- a/apps/sharebymail/l10n/fr.json +++ b/apps/sharebymail/l10n/fr.json @@ -51,5 +51,5 @@ "Reply to initiator" : "Répondre à l'initiateur", "We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again." : "Nous ne pouvons pas vous envoyer le mot de passe généré automatiquement. Veuillez renseigner une adresse e-mail valide dans vos paramètres personnels puis réessayer.", "Enforce password protection" : "Imposer la protection par mot de passe" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/systemtags/l10n/fr.js b/apps/systemtags/l10n/fr.js index 381c6c41eb9..8301f0182b4 100644 --- a/apps/systemtags/l10n/fr.js +++ b/apps/systemtags/l10n/fr.js @@ -64,4 +64,4 @@ OC.L10N.register( "Size" : "Taille", "Modified" : "Modifié" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/systemtags/l10n/fr.json b/apps/systemtags/l10n/fr.json index cc20ad1d967..a132c5176aa 100644 --- a/apps/systemtags/l10n/fr.json +++ b/apps/systemtags/l10n/fr.json @@ -61,5 +61,5 @@ "No entries found in this folder" : "Aucune entrée trouvée dans ce dossier", "Size" : "Taille", "Modified" : "Modifié" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/theming/l10n/fr.js b/apps/theming/l10n/fr.js index 1165934ccec..8b8193e3c34 100644 --- a/apps/theming/l10n/fr.js +++ b/apps/theming/l10n/fr.js @@ -51,4 +51,4 @@ OC.L10N.register( "Upload new favicon" : "Téléverser un nouveau favicon", "Install the Imagemagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color." : "Installez l'extension PHP Imagemagick avec le support pour les images SVG afin de générer automatiquement les favicons sur base du logo téléversé et de la couleur." }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/theming/l10n/fr.json b/apps/theming/l10n/fr.json index 8e0e69e3f03..37aa5065982 100644 --- a/apps/theming/l10n/fr.json +++ b/apps/theming/l10n/fr.json @@ -48,5 +48,5 @@ "Favicon" : "Favicon", "Upload new favicon" : "Téléverser un nouveau favicon", "Install the Imagemagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color." : "Installez l'extension PHP Imagemagick avec le support pour les images SVG afin de générer automatiquement les favicons sur base du logo téléversé et de la couleur." -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/twofactor_backupcodes/l10n/fr.js b/apps/twofactor_backupcodes/l10n/fr.js index 8f44815a5f8..31aefb65a68 100644 --- a/apps/twofactor_backupcodes/l10n/fr.js +++ b/apps/twofactor_backupcodes/l10n/fr.js @@ -20,4 +20,4 @@ OC.L10N.register( "Use one of the backup codes you saved when setting up two-factor authentication." : "Utilisez l'un des codes de secours que vous avez crée lors du paramétrage de l'authentification à deux facteurs.", "Submit" : "Envoyer" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/twofactor_backupcodes/l10n/fr.json b/apps/twofactor_backupcodes/l10n/fr.json index 48157e74a03..4e31f7462f1 100644 --- a/apps/twofactor_backupcodes/l10n/fr.json +++ b/apps/twofactor_backupcodes/l10n/fr.json @@ -17,5 +17,5 @@ "{name} backup codes" : "{name} codes de récupération", "Use one of the backup codes you saved when setting up two-factor authentication." : "Utilisez l'un des codes de secours que vous avez crée lors du paramétrage de l'authentification à deux facteurs.", "Submit" : "Envoyer" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/fr.js b/apps/updatenotification/l10n/fr.js index 10975f22db1..dfd80eee223 100644 --- a/apps/updatenotification/l10n/fr.js +++ b/apps/updatenotification/l10n/fr.js @@ -52,4 +52,4 @@ OC.L10N.register( "<strong>All</strong> apps have a compatible version for this Nextcloud version available" : "<strong></strong>Applications compatibles pour cette version de Nexctloud", "_<strong>%n</strong> app has no compatible version for this Nextcloud version available_::_<strong>%n</strong> apps have no compatible version for this Nextcloud version available_" : ["<strong>%n</strong> application n'a pas de version compatible avec cette version disponible de Nextcloud","<strong>%n</strong> applications n'ont pas de version compatible avec cette version disponible de Nextcloud"] }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/updatenotification/l10n/fr.json b/apps/updatenotification/l10n/fr.json index e5948800f77..714a2e3770b 100644 --- a/apps/updatenotification/l10n/fr.json +++ b/apps/updatenotification/l10n/fr.json @@ -49,5 +49,5 @@ "_<strong>%n</strong> app has no update for this version available_::_<strong>%n</strong> apps have no update for this version available_" : ["<strong>%n</strong> application n'a pas de mise à jour disponible pour cette version","<strong>%n</strong> applications n'ont pas de mise à jour disponible pour cette version"], "<strong>All</strong> apps have a compatible version for this Nextcloud version available" : "<strong></strong>Applications compatibles pour cette version de Nexctloud", "_<strong>%n</strong> app has no compatible version for this Nextcloud version available_::_<strong>%n</strong> apps have no compatible version for this Nextcloud version available_" : ["<strong>%n</strong> application n'a pas de version compatible avec cette version disponible de Nextcloud","<strong>%n</strong> applications n'ont pas de version compatible avec cette version disponible de Nextcloud"] -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/user_ldap/appinfo/info.xml b/apps/user_ldap/appinfo/info.xml index 2dae4845241..d654d34d66e 100644 --- a/apps/user_ldap/appinfo/info.xml +++ b/apps/user_ldap/appinfo/info.xml @@ -50,6 +50,7 @@ A user logs into Nextcloud with their LDAP or AD credentials, and is granted acc <command>OCA\User_LDAP\Command\CheckUser</command> <command>OCA\User_LDAP\Command\CreateEmptyConfig</command> <command>OCA\User_LDAP\Command\DeleteConfig</command> + <command>OCA\User_LDAP\Command\ResetGroup</command> <command>OCA\User_LDAP\Command\ResetUser</command> <command>OCA\User_LDAP\Command\Search</command> <command>OCA\User_LDAP\Command\SetConfig</command> diff --git a/apps/user_ldap/composer/composer/autoload_classmap.php b/apps/user_ldap/composer/composer/autoload_classmap.php index 12ede37a941..ae112b2b604 100644 --- a/apps/user_ldap/composer/composer/autoload_classmap.php +++ b/apps/user_ldap/composer/composer/autoload_classmap.php @@ -14,6 +14,7 @@ return array( 'OCA\\User_LDAP\\Command\\CheckUser' => $baseDir . '/../lib/Command/CheckUser.php', 'OCA\\User_LDAP\\Command\\CreateEmptyConfig' => $baseDir . '/../lib/Command/CreateEmptyConfig.php', 'OCA\\User_LDAP\\Command\\DeleteConfig' => $baseDir . '/../lib/Command/DeleteConfig.php', + 'OCA\\User_LDAP\\Command\\ResetGroup' => $baseDir . '/../lib/Command/ResetGroup.php', 'OCA\\User_LDAP\\Command\\ResetUser' => $baseDir . '/../lib/Command/ResetUser.php', 'OCA\\User_LDAP\\Command\\Search' => $baseDir . '/../lib/Command/Search.php', 'OCA\\User_LDAP\\Command\\SetConfig' => $baseDir . '/../lib/Command/SetConfig.php', @@ -26,6 +27,7 @@ return array( 'OCA\\User_LDAP\\ConnectionFactory' => $baseDir . '/../lib/ConnectionFactory.php', 'OCA\\User_LDAP\\Controller\\ConfigAPIController' => $baseDir . '/../lib/Controller/ConfigAPIController.php', 'OCA\\User_LDAP\\Controller\\RenewPasswordController' => $baseDir . '/../lib/Controller/RenewPasswordController.php', + 'OCA\\User_LDAP\\DataCollector\\LdapDataCollector' => $baseDir . '/../lib/DataCollector/LdapDataCollector.php', 'OCA\\User_LDAP\\Events\\GroupBackendRegistered' => $baseDir . '/../lib/Events/GroupBackendRegistered.php', 'OCA\\User_LDAP\\Events\\UserBackendRegistered' => $baseDir . '/../lib/Events/UserBackendRegistered.php', 'OCA\\User_LDAP\\Exceptions\\AttributeNotSet' => $baseDir . '/../lib/Exceptions/AttributeNotSet.php', diff --git a/apps/user_ldap/composer/composer/autoload_static.php b/apps/user_ldap/composer/composer/autoload_static.php index ecf5e4167f6..3ff92c350f5 100644 --- a/apps/user_ldap/composer/composer/autoload_static.php +++ b/apps/user_ldap/composer/composer/autoload_static.php @@ -29,6 +29,7 @@ class ComposerStaticInitUser_LDAP 'OCA\\User_LDAP\\Command\\CheckUser' => __DIR__ . '/..' . '/../lib/Command/CheckUser.php', 'OCA\\User_LDAP\\Command\\CreateEmptyConfig' => __DIR__ . '/..' . '/../lib/Command/CreateEmptyConfig.php', 'OCA\\User_LDAP\\Command\\DeleteConfig' => __DIR__ . '/..' . '/../lib/Command/DeleteConfig.php', + 'OCA\\User_LDAP\\Command\\ResetGroup' => __DIR__ . '/..' . '/../lib/Command/ResetGroup.php', 'OCA\\User_LDAP\\Command\\ResetUser' => __DIR__ . '/..' . '/../lib/Command/ResetUser.php', 'OCA\\User_LDAP\\Command\\Search' => __DIR__ . '/..' . '/../lib/Command/Search.php', 'OCA\\User_LDAP\\Command\\SetConfig' => __DIR__ . '/..' . '/../lib/Command/SetConfig.php', @@ -41,6 +42,7 @@ class ComposerStaticInitUser_LDAP 'OCA\\User_LDAP\\ConnectionFactory' => __DIR__ . '/..' . '/../lib/ConnectionFactory.php', 'OCA\\User_LDAP\\Controller\\ConfigAPIController' => __DIR__ . '/..' . '/../lib/Controller/ConfigAPIController.php', 'OCA\\User_LDAP\\Controller\\RenewPasswordController' => __DIR__ . '/..' . '/../lib/Controller/RenewPasswordController.php', + 'OCA\\User_LDAP\\DataCollector\\LdapDataCollector' => __DIR__ . '/..' . '/../lib/DataCollector/LdapDataCollector.php', 'OCA\\User_LDAP\\Events\\GroupBackendRegistered' => __DIR__ . '/..' . '/../lib/Events/GroupBackendRegistered.php', 'OCA\\User_LDAP\\Events\\UserBackendRegistered' => __DIR__ . '/..' . '/../lib/Events/UserBackendRegistered.php', 'OCA\\User_LDAP\\Exceptions\\AttributeNotSet' => __DIR__ . '/..' . '/../lib/Exceptions/AttributeNotSet.php', diff --git a/apps/user_ldap/l10n/fr.js b/apps/user_ldap/l10n/fr.js index 8d1fa7d2ae2..369808d859d 100644 --- a/apps/user_ldap/l10n/fr.js +++ b/apps/user_ldap/l10n/fr.js @@ -199,4 +199,4 @@ OC.L10N.register( "LDAP / AD Email Address:" : "Adresse mail LDAP / AD :", "By default the internal username will be created from the UUID attribute. It makes sure that the username is unique and characters do not need to be converted. The internal username has the restriction that only these characters are allowed: [ a-zA-Z0-9_.@- ]. Other characters are replaced with their ASCII correspondence or simply omitted. On collisions a number will be added/increased. The internal username is used to identify a user internally. It is also the default name for the user home folder. It is also a part of remote URLs, for instance for all *DAV services. With this setting, the default behavior can be overridden. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users." : "Par défaut le nom d'utilisateur interne sera créé depuis l'attribut UUID. Cela permet de s'assurer que le nom d'utilisateur est unique et que les caractères n'ont pas besoin d'être convertis. Le nom d'utilisateur interne a pour restriction de ne contenir que les caractères suivants : [ a-zA-Z0-9_.@- ]. Les autres caractères sont remplacés par leurs correspondants ASCII ou simplement omis. En cas de collisions, un nombre sera ajouté/incrémenté. Le nom d'utilisateur interne est utilisé pour identifier un utilisateur en interne. C'est aussi le nom par défaut du dossier personnel de l'utilisateur. Il fait aussi parti des URLs distantes, par exemple pour tous les services *DAV. Avec ce paramètre, le comportement par défaut peut être écrasé. Laissez le vide pour utiliser le comportement par défaut. Les modifications prendront effet seulement pour les nouveaux utilisateurs LDAP mappés (ajoutés)." }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/user_ldap/l10n/fr.json b/apps/user_ldap/l10n/fr.json index a6990467d5d..701ce8dfb78 100644 --- a/apps/user_ldap/l10n/fr.json +++ b/apps/user_ldap/l10n/fr.json @@ -196,5 +196,5 @@ "Allows login against the LDAP / AD username, which is either \"uid\" or \"sAMAccountName\" and will be detected." : "Autorise l'authentification à partir du nom d'utilisateur LDAP / AD.Celui-ci sera détecté et pourra être soit \"uid\", soit \"sAMAccountName\".", "LDAP / AD Email Address:" : "Adresse mail LDAP / AD :", "By default the internal username will be created from the UUID attribute. It makes sure that the username is unique and characters do not need to be converted. The internal username has the restriction that only these characters are allowed: [ a-zA-Z0-9_.@- ]. Other characters are replaced with their ASCII correspondence or simply omitted. On collisions a number will be added/increased. The internal username is used to identify a user internally. It is also the default name for the user home folder. It is also a part of remote URLs, for instance for all *DAV services. With this setting, the default behavior can be overridden. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users." : "Par défaut le nom d'utilisateur interne sera créé depuis l'attribut UUID. Cela permet de s'assurer que le nom d'utilisateur est unique et que les caractères n'ont pas besoin d'être convertis. Le nom d'utilisateur interne a pour restriction de ne contenir que les caractères suivants : [ a-zA-Z0-9_.@- ]. Les autres caractères sont remplacés par leurs correspondants ASCII ou simplement omis. En cas de collisions, un nombre sera ajouté/incrémenté. Le nom d'utilisateur interne est utilisé pour identifier un utilisateur en interne. C'est aussi le nom par défaut du dossier personnel de l'utilisateur. Il fait aussi parti des URLs distantes, par exemple pour tous les services *DAV. Avec ce paramètre, le comportement par défaut peut être écrasé. Laissez le vide pour utiliser le comportement par défaut. Les modifications prendront effet seulement pour les nouveaux utilisateurs LDAP mappés (ajoutés)." -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/user_ldap/lib/Command/ResetGroup.php b/apps/user_ldap/lib/Command/ResetGroup.php new file mode 100644 index 00000000000..f3c3019f919 --- /dev/null +++ b/apps/user_ldap/lib/Command/ResetGroup.php @@ -0,0 +1,111 @@ +<?php +/** + * @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * @author Côme Chilliet <come.chilliet@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\User_LDAP\Command; + +use OCA\User_LDAP\Group_Proxy; +use OCA\User_LDAP\GroupPluginManager; +use OCP\IGroup; +use OCP\IGroupManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; + +class ResetGroup extends Command { + private IGroupManager $groupManager; + private GroupPluginManager $pluginManager; + private Group_Proxy $backend; + + public function __construct( + IGroupManager $groupManager, + GroupPluginManager $pluginManager, + Group_Proxy $backend + ) { + $this->groupManager = $groupManager; + $this->pluginManager = $pluginManager; + $this->backend = $backend; + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ldap:reset-group') + ->setDescription('deletes an LDAP group independent of the group state in the LDAP') + ->addArgument( + 'gid', + InputArgument::REQUIRED, + 'the group name as used in Nextcloud' + ) + ->addOption( + 'yes', + 'y', + InputOption::VALUE_NONE, + 'do not ask for confirmation' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $gid = $input->getArgument('gid'); + $group = $this->groupManager->get($gid); + if (!$group instanceof IGroup) { + throw new \Exception('Group not found'); + } + $backends = $group->getBackendNames(); + if (!in_array('LDAP', $backends)) { + throw new \Exception('The given group is not a recognized LDAP group.'); + } + if ($input->getOption('yes') === false) { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $q = new Question('Delete all local data of this group (y|N)? '); + $input->setOption('yes', $helper->ask($input, $output, $q) === 'y'); + } + if ($input->getOption('yes') !== true) { + throw new \Exception('Reset cancelled by operator'); + } + + // Disable real deletion if a plugin supports it + $pluginManagerSuppressed = $this->pluginManager->setSuppressDeletion(true); + // Bypass groupExists test to force mapping deletion + $this->backend->getLDAPAccess($gid)->connection->writeToCache('groupExists' . $gid, false); + echo "calling delete $gid\n"; + if ($group->delete()) { + $this->pluginManager->setSuppressDeletion($pluginManagerSuppressed); + return 0; + } + } catch (\Throwable $e) { + if (isset($pluginManagerSuppressed)) { + $this->pluginManager->setSuppressDeletion($pluginManagerSuppressed); + } + $output->writeln('<error>' . $e->getMessage() . '</error>'); + return 1; + } + $output->writeln('<error>Error while resetting group</error>'); + return 2; + } +} diff --git a/apps/user_ldap/lib/DataCollector/LdapDataCollector.php b/apps/user_ldap/lib/DataCollector/LdapDataCollector.php new file mode 100644 index 00000000000..cb61de96e37 --- /dev/null +++ b/apps/user_ldap/lib/DataCollector/LdapDataCollector.php @@ -0,0 +1,50 @@ +<?php declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\User_LDAP\DataCollector; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\AbstractDataCollector; + +class LdapDataCollector extends AbstractDataCollector { + public function startLdapRequest(string $query, array $args): void { + $this->data[] = [ + 'start' => microtime(true), + 'query' => $query, + 'args' => $args, + 'end' => microtime(true), + ]; + } + + public function stopLastLdapRequest(): void { + $this->data[count($this->data) - 1]['end'] = microtime(true); + } + + public function getName(): string { + return 'ldap'; + } + + public function collect(Request $request, Response $response, \Throwable $exception = null): void { + } +} diff --git a/apps/user_ldap/lib/GroupPluginManager.php b/apps/user_ldap/lib/GroupPluginManager.php index d23e9d4d443..5999409cdba 100644 --- a/apps/user_ldap/lib/GroupPluginManager.php +++ b/apps/user_ldap/lib/GroupPluginManager.php @@ -26,9 +26,10 @@ namespace OCA\User_LDAP; use OCP\GroupInterface; class GroupPluginManager { - private $respondToActions = 0; + private int $respondToActions = 0; - private $which = [ + /** @var array<int, ?ILDAPGroupPlugin> */ + private array $which = [ GroupInterface::CREATE_GROUP => null, GroupInterface::DELETE_GROUP => null, GroupInterface::ADD_TO_GROUP => null, @@ -37,6 +38,8 @@ class GroupPluginManager { GroupInterface::GROUP_DETAILS => null ]; + private bool $suppressDeletion = false; + /** * @return int All implemented actions */ @@ -84,16 +87,31 @@ class GroupPluginManager { throw new \Exception('No plugin implements createGroup in this LDAP Backend.'); } + public function canDeleteGroup(): bool { + return !$this->suppressDeletion && $this->implementsActions(GroupInterface::DELETE_GROUP); + } + + /** + * @return bool – the value before the change + */ + public function setSuppressDeletion(bool $value): bool { + $old = $this->suppressDeletion; + $this->suppressDeletion = $value; + return $old; + } + /** * Delete a group - * @param string $gid Group Id of the group to delete - * @return bool + * * @throws \Exception */ - public function deleteGroup($gid) { + public function deleteGroup(string $gid): bool { $plugin = $this->which[GroupInterface::DELETE_GROUP]; if ($plugin) { + if ($this->suppressDeletion) { + return false; + } return $plugin->deleteGroup($gid); } throw new \Exception('No plugin implements deleteGroup in this LDAP Backend.'); diff --git a/apps/user_ldap/lib/Group_LDAP.php b/apps/user_ldap/lib/Group_LDAP.php index 766b77bf521..f9d9b061743 100644 --- a/apps/user_ldap/lib/Group_LDAP.php +++ b/apps/user_ldap/lib/Group_LDAP.php @@ -48,10 +48,11 @@ use OC; use OC\Cache\CappedMemoryCache; use OC\ServerNotAvailableException; use OCP\Group\Backend\IGetDisplayNameBackend; +use OCP\Group\Backend\IDeleteGroupBackend; use OCP\GroupInterface; use Psr\Log\LoggerInterface; -class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend { +class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, IDeleteGroupBackend { protected $enabled = false; /** @var string[][] $cachedGroupMembers array of users with gid as key */ @@ -1204,6 +1205,7 @@ class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, I */ public function implementsActions($actions) { return (bool)((GroupInterface::COUNT_USERS | + GroupInterface::DELETE_GROUP | $this->groupPluginManager->getImplementedActions()) & $actions); } @@ -1249,19 +1251,32 @@ class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, I * delete a group * * @param string $gid gid of the group to delete - * @return bool * @throws Exception */ - public function deleteGroup($gid) { - if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) { + public function deleteGroup(string $gid): bool { + if ($this->groupPluginManager->canDeleteGroup()) { if ($ret = $this->groupPluginManager->deleteGroup($gid)) { - #delete group in nextcloud internal db + // Delete group in nextcloud internal db $this->access->getGroupMapper()->unmap($gid); $this->access->connection->writeToCache("groupExists" . $gid, false); } return $ret; } - throw new Exception('Could not delete group in LDAP backend.'); + + // Getting dn, if false the group is not mapped + $dn = $this->access->groupname2dn($gid); + if (!$dn) { + throw new Exception('Could not delete unknown group '.$gid.' in LDAP backend.'); + } + + if (!$this->groupExists($gid)) { + // The group does not exist in the LDAP, remove the mapping + $this->access->getGroupMapper()->unmap($gid); + $this->access->connection->writeToCache("groupExists" . $gid, false); + return true; + } + + throw new Exception('Could not delete existing group '.$gid.' in LDAP backend.'); } /** diff --git a/apps/user_ldap/lib/Group_Proxy.php b/apps/user_ldap/lib/Group_Proxy.php index 92a9041949e..ea2fcce679c 100644 --- a/apps/user_ldap/lib/Group_Proxy.php +++ b/apps/user_ldap/lib/Group_Proxy.php @@ -28,10 +28,11 @@ */ namespace OCA\User_LDAP; -use OCP\Group\Backend\INamedBackend; +use OCP\Group\Backend\IDeleteGroupBackend; use OCP\Group\Backend\IGetDisplayNameBackend; +use OCP\Group\Backend\INamedBackend; -class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend { +class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend, IDeleteGroupBackend { private $backends = []; private $refBackend = null; @@ -171,11 +172,8 @@ class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGet /** * delete a group - * - * @param string $gid gid of the group to delete - * @return bool */ - public function deleteGroup($gid) { + public function deleteGroup(string $gid): bool { return $this->handleRequest( $gid, 'deleteGroup', [$gid]); } diff --git a/apps/user_ldap/lib/LDAP.php b/apps/user_ldap/lib/LDAP.php index 18a9476128d..3c579596941 100644 --- a/apps/user_ldap/lib/LDAP.php +++ b/apps/user_ldap/lib/LDAP.php @@ -14,6 +14,7 @@ * @author Robin McCorkell <robin@mccorkell.me.uk> * @author Roeland Jago Douma <roeland@famdouma.nl> * @author Roger Szabo <roger.szabo@web.de> + * @author Carl Schwan <carl@carlschwan.eu> * * @license AGPL-3.0 * @@ -32,7 +33,9 @@ */ namespace OCA\User_LDAP; +use OCP\Profiler\IProfiler; use OC\ServerNotAvailableException; +use OCA\User_LDAP\DataCollector\LdapDataCollector; use OCA\User_LDAP\Exceptions\ConstraintViolationException; use OCA\User_LDAP\PagedResults\IAdapter; use OCA\User_LDAP\PagedResults\Php73; @@ -45,9 +48,18 @@ class LDAP implements ILDAPWrapper { /** @var IAdapter */ protected $pagedResultsAdapter; + private ?LdapDataCollector $dataCollector = null; + public function __construct(string $logFile = '') { $this->pagedResultsAdapter = new Php73(); $this->logFile = $logFile; + + /** @var IProfiler $profiler */ + $profiler = \OC::$server->get(IProfiler::class); + if ($profiler->isEnabled()) { + $this->dataCollector = new LdapDataCollector(); + $profiler->add($this->dataCollector); + } } /** @@ -295,24 +307,26 @@ class LDAP implements ILDAPWrapper { if ($this->isResultFalse($result)) { $this->postFunctionCall(); } + if ($this->dataCollector !== null) { + $this->dataCollector->stopLastLdapRequest(); + } return $result; } return null; } - /** - * @param string $functionName - * @param array $args - */ - private function preFunctionCall($functionName, $args) { + private function preFunctionCall(string $functionName, array $args): void { $this->curFunc = $functionName; $this->curArgs = $args; + if ($this->dataCollector !== null) { + $args = array_map(fn ($item) => (!$this->isResource($item) ? $item : '(resource)'), $this->curArgs); + + $this->dataCollector->startLdapRequest($this->curFunc, $args); + } + if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { - $args = array_reduce($this->curArgs, static function (array $carry, $item): array { - $carry[] = !is_resource($item) ? $item : '(resource)'; - return $carry; - }, []); + $args = array_map(fn ($item) => (!$this->isResource($item) ? $item : '(resource)'), $this->curArgs); file_put_contents( $this->logFile, $this->curFunc . '::' . json_encode($args) . "\n", diff --git a/apps/user_ldap/lib/UserPluginManager.php b/apps/user_ldap/lib/UserPluginManager.php index 035b7952dce..748a210cf60 100644 --- a/apps/user_ldap/lib/UserPluginManager.php +++ b/apps/user_ldap/lib/UserPluginManager.php @@ -28,9 +28,9 @@ namespace OCA\User_LDAP; use OC\User\Backend; class UserPluginManager { - private $respondToActions = 0; + private int $respondToActions = 0; - private $which = [ + private array $which = [ Backend::CREATE_USER => null, Backend::SET_PASSWORD => null, Backend::GET_HOME => null, @@ -41,8 +41,7 @@ class UserPluginManager { 'deleteUser' => null ]; - /** @var bool */ - private $suppressDeletion = false; + private bool $suppressDeletion = false; /** * @return int All implemented actions, except for 'deleteUser' diff --git a/apps/user_ldap/tests/GroupLDAPPluginTest.php b/apps/user_ldap/tests/GroupLDAPPluginTest.php index b55f57990a0..660608d6b1f 100644 --- a/apps/user_ldap/tests/GroupLDAPPluginTest.php +++ b/apps/user_ldap/tests/GroupLDAPPluginTest.php @@ -84,7 +84,7 @@ class GroupLDAPPluginTest extends \Test\TestCase { $pluginManager->createGroup('group'); } - + public function testCreateGroupNotRegistered() { $this->expectException(\Exception::class); $this->expectExceptionMessage('No plugin implements createGroup in this LDAP Backend.'); @@ -108,13 +108,13 @@ class GroupLDAPPluginTest extends \Test\TestCase { ->method('deleteGroup') ->with( $this->equalTo('group') - ); + )->willReturn(true); $pluginManager->register($plugin); - $pluginManager->deleteGroup('group'); + $this->assertTrue($pluginManager->deleteGroup('group')); } - + public function testDeleteGroupNotRegistered() { $this->expectException(\Exception::class); $this->expectExceptionMessage('No plugin implements deleteGroup in this LDAP Backend.'); @@ -145,7 +145,7 @@ class GroupLDAPPluginTest extends \Test\TestCase { $pluginManager->addToGroup('uid', 'gid'); } - + public function testAddToGroupNotRegistered() { $this->expectException(\Exception::class); $this->expectExceptionMessage('No plugin implements addToGroup in this LDAP Backend.'); @@ -176,7 +176,7 @@ class GroupLDAPPluginTest extends \Test\TestCase { $pluginManager->removeFromGroup('uid', 'gid'); } - + public function testRemoveFromGroupNotRegistered() { $this->expectException(\Exception::class); $this->expectExceptionMessage('No plugin implements removeFromGroup in this LDAP Backend.'); @@ -207,7 +207,7 @@ class GroupLDAPPluginTest extends \Test\TestCase { $pluginManager->countUsersInGroup('gid', 'search'); } - + public function testCountUsersInGroupNotRegistered() { $this->expectException(\Exception::class); $this->expectExceptionMessage('No plugin implements countUsersInGroup in this LDAP Backend.'); @@ -237,7 +237,7 @@ class GroupLDAPPluginTest extends \Test\TestCase { $pluginManager->getGroupDetails('gid'); } - + public function testgetGroupDetailsNotRegistered() { $this->expectException(\Exception::class); $this->expectExceptionMessage('No plugin implements getGroupDetails in this LDAP Backend.'); diff --git a/apps/user_ldap/tests/Group_LDAPTest.php b/apps/user_ldap/tests/Group_LDAPTest.php index f8327c0776c..6204c22cb9e 100644 --- a/apps/user_ldap/tests/Group_LDAPTest.php +++ b/apps/user_ldap/tests/Group_LDAPTest.php @@ -1092,7 +1092,7 @@ class Group_LDAPTest extends TestCase { $pluginManager->expects($this->once()) ->method('deleteGroup') ->with('gid') - ->willReturn('result'); + ->willReturn(true); $mapper = $this->getMockBuilder(GroupMapping::class) ->setMethods(['unmap']) @@ -1108,7 +1108,7 @@ class Group_LDAPTest extends TestCase { $ldap = new GroupLDAP($access, $pluginManager); - $this->assertEquals($ldap->deleteGroup('gid'), 'result'); + $this->assertTrue($ldap->deleteGroup('gid')); } diff --git a/apps/user_status/l10n/fr.js b/apps/user_status/l10n/fr.js index 01abc796d3c..4ad763c6852 100644 --- a/apps/user_status/l10n/fr.js +++ b/apps/user_status/l10n/fr.js @@ -37,4 +37,4 @@ OC.L10N.register( "Appear offline" : "Apparaitre hors-ligne", "What's your status?" : "Quel est votre statut ?" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/user_status/l10n/fr.json b/apps/user_status/l10n/fr.json index a62900e8f1f..8b0dbe805f0 100644 --- a/apps/user_status/l10n/fr.json +++ b/apps/user_status/l10n/fr.json @@ -34,5 +34,5 @@ "Mute all notifications" : "Désactiver toutes les notifications", "Appear offline" : "Apparaitre hors-ligne", "What's your status?" : "Quel est votre statut ?" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/weather_status/l10n/fr.js b/apps/weather_status/l10n/fr.js index 2d6500f1890..ec78c344202 100644 --- a/apps/weather_status/l10n/fr.js +++ b/apps/weather_status/l10n/fr.js @@ -47,4 +47,4 @@ OC.L10N.register( "{temperature} {unit} Light rain showers at {time}" : "{temperature} {unit} Faibles averses à {time}", "{temperature} {unit} Heavy rain showers at {time}" : "{temperature} {unit} Fortes averses à {time}" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/weather_status/l10n/fr.json b/apps/weather_status/l10n/fr.json index f655d207345..969a8fa3a99 100644 --- a/apps/weather_status/l10n/fr.json +++ b/apps/weather_status/l10n/fr.json @@ -44,5 +44,5 @@ "{temperature} {unit} Rain showers at {time}" : "{temperature} {unit} Averses à {time}", "{temperature} {unit} Light rain showers at {time}" : "{temperature} {unit} Faibles averses à {time}", "{temperature} {unit} Heavy rain showers at {time}" : "{temperature} {unit} Fortes averses à {time}" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/weather_status/l10n/pl.js b/apps/weather_status/l10n/pl.js index 20bca6a2fe8..02f2e6756bb 100644 --- a/apps/weather_status/l10n/pl.js +++ b/apps/weather_status/l10n/pl.js @@ -21,6 +21,18 @@ OC.L10N.register( "{temperature} {unit} partly cloudy" : "{temperature} {unit} częściowe zachmurzenie", "{temperature} {unit} foggy later today" : "{temperature} {unit} mgła później dzisiaj", "{temperature} {unit} foggy" : "{temperature} {unit} mgła", + "{temperature} {unit} light rainfall later today" : "{temperature} {unit} lekkie opady deszczu później dzisiaj", + "{temperature} {unit} light rainfall" : "{temperature} {unit} lekkie opady deszczu", + "{temperature} {unit} rainfall later today" : "{temperature} {unit} opady deszczu później dzisiaj", + "{temperature} {unit} rainfall" : "{temperature} {unit} opady deszczu", + "{temperature} {unit} heavy rainfall later today" : "{temperature} {unit} ulewne opady deszczu później dzisiaj", + "{temperature} {unit} heavy rainfall" : "{temperature} {unit} ulewne opady deszczu", + "{temperature} {unit} rainfall showers later today" : "{temperature} {unit} przelotne opady deszczu później dzisiaj", + "{temperature} {unit} rainfall showers" : "{temperature} {unit} przelotne opady deszczu", + "{temperature} {unit} light rainfall showers later today" : "{temperature} {unit} lekkie przelotne opady deszczu później dzisiaj", + "{temperature} {unit} light rainfall showers" : "{temperature} {unit} lekkie przelotne opady deszczu", + "{temperature} {unit} heavy rainfall showers later today" : "{temperature} {unit} obfite opady deszczu później dzisiaj", + "{temperature} {unit} heavy rainfall showers" : "{temperature} {unit} ulewne opady deszczu", "More weather for {adr}" : "Więcej pogody dla {adr}", "Loading weather" : "Wczytywanie pogody", "Remove from favorites" : "Usuń z ulubionych", diff --git a/apps/weather_status/l10n/pl.json b/apps/weather_status/l10n/pl.json index 597c8cc449e..5cebe9bcf35 100644 --- a/apps/weather_status/l10n/pl.json +++ b/apps/weather_status/l10n/pl.json @@ -19,6 +19,18 @@ "{temperature} {unit} partly cloudy" : "{temperature} {unit} częściowe zachmurzenie", "{temperature} {unit} foggy later today" : "{temperature} {unit} mgła później dzisiaj", "{temperature} {unit} foggy" : "{temperature} {unit} mgła", + "{temperature} {unit} light rainfall later today" : "{temperature} {unit} lekkie opady deszczu później dzisiaj", + "{temperature} {unit} light rainfall" : "{temperature} {unit} lekkie opady deszczu", + "{temperature} {unit} rainfall later today" : "{temperature} {unit} opady deszczu później dzisiaj", + "{temperature} {unit} rainfall" : "{temperature} {unit} opady deszczu", + "{temperature} {unit} heavy rainfall later today" : "{temperature} {unit} ulewne opady deszczu później dzisiaj", + "{temperature} {unit} heavy rainfall" : "{temperature} {unit} ulewne opady deszczu", + "{temperature} {unit} rainfall showers later today" : "{temperature} {unit} przelotne opady deszczu później dzisiaj", + "{temperature} {unit} rainfall showers" : "{temperature} {unit} przelotne opady deszczu", + "{temperature} {unit} light rainfall showers later today" : "{temperature} {unit} lekkie przelotne opady deszczu później dzisiaj", + "{temperature} {unit} light rainfall showers" : "{temperature} {unit} lekkie przelotne opady deszczu", + "{temperature} {unit} heavy rainfall showers later today" : "{temperature} {unit} obfite opady deszczu później dzisiaj", + "{temperature} {unit} heavy rainfall showers" : "{temperature} {unit} ulewne opady deszczu", "More weather for {adr}" : "Więcej pogody dla {adr}", "Loading weather" : "Wczytywanie pogody", "Remove from favorites" : "Usuń z ulubionych", diff --git a/apps/workflowengine/l10n/fr.js b/apps/workflowengine/l10n/fr.js index 9e8b3b53280..655eeb7772f 100644 --- a/apps/workflowengine/l10n/fr.js +++ b/apps/workflowengine/l10n/fr.js @@ -117,4 +117,4 @@ OC.L10N.register( "is not member of" : "n'est pas membre de", "Browse the app store" : "Parcourir l'App Store" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/workflowengine/l10n/fr.json b/apps/workflowengine/l10n/fr.json index ce154bf9794..1286dc5262a 100644 --- a/apps/workflowengine/l10n/fr.json +++ b/apps/workflowengine/l10n/fr.json @@ -114,5 +114,5 @@ "is member of" : "est membre de", "is not member of" : "n'est pas membre de", "Browse the app store" : "Parcourir l'App Store" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/build/psalm-baseline.xml b/build/psalm-baseline.xml index e721a490e6d..8884bc18612 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -1023,7 +1023,6 @@ </RedundantCondition> <TypeDoesNotContainType occurrences="2"> <code>get_class($res) === 'OpenSSLAsymmetricKey'</code> - <code>is_object($res)</code> </TypeDoesNotContainType> </file> <file src="apps/encryption/lib/Crypto/EncryptAll.php"> @@ -2638,11 +2637,6 @@ <code>$default</code> </MoreSpecificImplementedParamType> </file> - <file src="lib/private/AppFramework/Utility/SimpleContainer.php"> - <UndefinedMethod occurrences="1"> - <code>getName</code> - </UndefinedMethod> - </file> <file src="lib/private/Archive/TAR.php"> <UndefinedDocblockClass occurrences="1"> <code>$this->tar->extractInString($path)</code> diff --git a/config/config.sample.php b/config/config.sample.php index 357321f6d29..7a7dfa356b8 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -962,6 +962,14 @@ $CONFIG = [ */ 'log_rotate_size' => 100 * 1024 * 1024, +/** + * Enable built-in profiler. Helpful when trying to debug performance + * issues. + * + * Note that this has a performance impact and shouldn't be enabled + * on production. + */ +'profiler' => false, /** * Alternate Code Locations diff --git a/core/Command/Group/Delete.php b/core/Command/Group/Delete.php index 75202308944..be97be83407 100644 --- a/core/Command/Group/Delete.php +++ b/core/Command/Group/Delete.php @@ -61,7 +61,7 @@ class Delete extends Base { $output->writeln('<error>Group "' . $gid . '" could not be deleted.</error>'); return 1; } - if (! $this->groupManager->groupExists($gid)) { + if (!$this->groupManager->groupExists($gid)) { $output->writeln('<error>Group "' . $gid . '" does not exist.</error>'); return 1; } diff --git a/core/Command/User/AddAppPassword.php b/core/Command/User/AddAppPassword.php index 7a2270e20b1..65b572533f5 100644 --- a/core/Command/User/AddAppPassword.php +++ b/core/Command/User/AddAppPassword.php @@ -1,4 +1,7 @@ <?php + +declare(strict_types=1); + /** * @copyright Copyright (c) 2020, NextCloud, Inc. * @@ -41,10 +44,13 @@ class AddAppPassword extends Command { /** @var IUserManager */ protected $userManager; + /** @var IProvider */ protected $tokenProvider; + /** @var ISecureRandom */ private $random; + /** @var IEventDispatcher */ private $eventDispatcher; @@ -72,13 +78,14 @@ class AddAppPassword extends Command { 'password-from-env', null, InputOption::VALUE_NONE, - 'read password from environment variable NC_PASS/OC_PASS' + 'Read password from environment variable NC_PASS/OC_PASS. Alternatively it will be asked for interactively or an app password without the login password will be created.' ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $username = $input->getArgument('user'); + $password = null; $user = $this->userManager->get($username); if (is_null($user)) { @@ -98,18 +105,13 @@ class AddAppPassword extends Command { $question = new Question('Enter the user password: '); $question->setHidden(true); + /** @var null|string $password */ $password = $helper->ask($input, $output, $question); - - if ($password === null) { - $output->writeln("<error>Password cannot be empty!</error>"); - return 1; - } - } else { - $output->writeln("<error>Interactive input or --password-from-env is needed for entering a new password!</error>"); - return 1; } - $output->writeln('<comment>The password has not been validated, some features might not work as intended.</comment>'); + if ($password === null) { + $output->writeln('<info>No password provided. The generated app password will therefore have limited capabilities. Any operation that requires the login password will fail.</info>'); + } $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); $generatedToken = $this->tokenProvider->generateToken( diff --git a/core/l10n/fr.js b/core/l10n/fr.js index d877fcb9a8f..383b62e0062 100644 --- a/core/l10n/fr.js +++ b/core/l10n/fr.js @@ -427,4 +427,4 @@ OC.L10N.register( "You haven't added any info yet" : "Vous n'avez pas encore ajouté de données", "{user} hasn't added any info yet" : "{user} n'a pas encore ajouté de données" }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/core/l10n/fr.json b/core/l10n/fr.json index 6d4fc36f4fe..26f16282e86 100644 --- a/core/l10n/fr.json +++ b/core/l10n/fr.json @@ -424,5 +424,5 @@ "To migrate to another database use the command line tool: 'occ db:convert-type', or see the {linkstart}documentation ↗{linkend}." : "Pour migrer vers une autre base de données, utiliser la ligne de commande : 'occ db:convert-type', ou se reporter à la {linkstart}documentation ↗{linkend}.", "You haven't added any info yet" : "Vous n'avez pas encore ajouté de données", "{user} hasn't added any info yet" : "{user} n'a pas encore ajouté de données" -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/core/l10n/hu.js b/core/l10n/hu.js index 5281ec32e93..0fd53c331b3 100644 --- a/core/l10n/hu.js +++ b/core/l10n/hu.js @@ -100,8 +100,10 @@ OC.L10N.register( "The PHP module \"imagick\" is not enabled although the theming app is. For favicon generation to work correctly, you need to install and enable this module." : "Az „imagick” PHP-modul nem engedélyezett, de a témázó alkalmazás igen. A webhelyikonok előállításához telepítenie és engedélyeznie kell ezt a modult.", "The PHP modules \"gmp\" and/or \"bcmath\" are not enabled. If you use WebAuthn passwordless authentication, these modules are required." : "A „gmp” vagy a „bcmath” PHP modulok nem engedélyezettek. Ha WebAuthn jelszó nélküli hitelesítést használ, akkor szükség van ezekre a modulokra.", "Module php-imagick in this instance has no SVG support. For better compatibility it is recommended to install it." : "A php-imagick modul ebben az esetben nem rendelkezik SVG támogatással. A jobb kompatibilitás érdekében ajánlott telepíteni.", + "Some columns in the database are missing a conversion to big int. Due to the fact that changing column types on big tables could take some time they were not changed automatically. By running \"occ db:convert-filecache-bigint\" those pending changes could be applied manually. This operation needs to be made while the instance is offline. For further details read {linkstart}the documentation page about this ↗{linkend}." : "Az adatbázis egyes oszlopaiból hiányzik a big int átalakítás. MIvel a nagy táblák oszloptípusainak megváltoztatása eltarthat egy ideig, azok nem lettek automatikusan megváltoztatva. Az „occ db: convert-filecache-bigint” futtatásával ezek a függőben lévő módosítások kézileg is alkalmazhatók. Ezt a műveletet offline állapotban kell végrehajtani. További részletekért olvassa el a {linkstart}erről szóló dokumentációs oldalt ↗{linkend}.", "SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend." : "Jelenleg SQLite van háttéradatbázisként használva. Nagyobb telepítésekhez javasoljuk, hogy váltson más háttéradatbázisra. ", "This is particularly recommended when using the desktop client for file synchronisation." : "Ezt különösen akkor javasoljuk, ha asztali klienst használ fájlszinkronizálásához.", + "To migrate to another database use the command line tool: \"occ db:convert-type\", or see the {linkstart}documentation ↗{linkend}." : "Más adatbázisba történő áttéréshez használja a parancssori eszközt: „occ db: convert-type”, vagy tekintse meg a {linkstart}dokumentációt ↗{linkend}.", "The PHP memory limit is below the recommended value of 512MB." : "A PHP memóriakorlátja az ajánlott 512 MB alatt van.", "Some app directories are owned by a different user than the web server one. This may be the case if apps have been installed manually. Check the permissions of the following app directories:" : "Néhány alkalmazáskönyvtár tulajdonosa különbözik a webkiszolgálóétól. Ez akkor fordul elő, ha az alkalmazás kézileg lett telepítve. Ellenőrizze az alábbi alkalmazáskönyvtárak jogosultságát:", "MySQL is used as database but does not support 4-byte characters. To be able to handle 4-byte characters (like emojis) without issues in filenames or comments for example it is recommended to enable the 4-byte support in MySQL. For further details read {linkstart}the documentation page about this ↗{linkend}." : "A MySQL adatbázis van használatban, de nem támogatja a 4 bájtos karaktereket. Hogy a 4 bájtos karakterek (például az emodzsikat) problémák nélkül kezelhetők legyenek, például a fájlnevekben vagy a megjegyzésekben, ajánlott engedélyezni a 4 bájtos támogatást a MySQL-ben. További részletekért olvassa el a {linkstart}erről szóló dokumentációs oldalt ↗{linkend}.", diff --git a/core/l10n/hu.json b/core/l10n/hu.json index 835b9d82ee7..077262166ed 100644 --- a/core/l10n/hu.json +++ b/core/l10n/hu.json @@ -98,8 +98,10 @@ "The PHP module \"imagick\" is not enabled although the theming app is. For favicon generation to work correctly, you need to install and enable this module." : "Az „imagick” PHP-modul nem engedélyezett, de a témázó alkalmazás igen. A webhelyikonok előállításához telepítenie és engedélyeznie kell ezt a modult.", "The PHP modules \"gmp\" and/or \"bcmath\" are not enabled. If you use WebAuthn passwordless authentication, these modules are required." : "A „gmp” vagy a „bcmath” PHP modulok nem engedélyezettek. Ha WebAuthn jelszó nélküli hitelesítést használ, akkor szükség van ezekre a modulokra.", "Module php-imagick in this instance has no SVG support. For better compatibility it is recommended to install it." : "A php-imagick modul ebben az esetben nem rendelkezik SVG támogatással. A jobb kompatibilitás érdekében ajánlott telepíteni.", + "Some columns in the database are missing a conversion to big int. Due to the fact that changing column types on big tables could take some time they were not changed automatically. By running \"occ db:convert-filecache-bigint\" those pending changes could be applied manually. This operation needs to be made while the instance is offline. For further details read {linkstart}the documentation page about this ↗{linkend}." : "Az adatbázis egyes oszlopaiból hiányzik a big int átalakítás. MIvel a nagy táblák oszloptípusainak megváltoztatása eltarthat egy ideig, azok nem lettek automatikusan megváltoztatva. Az „occ db: convert-filecache-bigint” futtatásával ezek a függőben lévő módosítások kézileg is alkalmazhatók. Ezt a műveletet offline állapotban kell végrehajtani. További részletekért olvassa el a {linkstart}erről szóló dokumentációs oldalt ↗{linkend}.", "SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend." : "Jelenleg SQLite van háttéradatbázisként használva. Nagyobb telepítésekhez javasoljuk, hogy váltson más háttéradatbázisra. ", "This is particularly recommended when using the desktop client for file synchronisation." : "Ezt különösen akkor javasoljuk, ha asztali klienst használ fájlszinkronizálásához.", + "To migrate to another database use the command line tool: \"occ db:convert-type\", or see the {linkstart}documentation ↗{linkend}." : "Más adatbázisba történő áttéréshez használja a parancssori eszközt: „occ db: convert-type”, vagy tekintse meg a {linkstart}dokumentációt ↗{linkend}.", "The PHP memory limit is below the recommended value of 512MB." : "A PHP memóriakorlátja az ajánlott 512 MB alatt van.", "Some app directories are owned by a different user than the web server one. This may be the case if apps have been installed manually. Check the permissions of the following app directories:" : "Néhány alkalmazáskönyvtár tulajdonosa különbözik a webkiszolgálóétól. Ez akkor fordul elő, ha az alkalmazás kézileg lett telepítve. Ellenőrizze az alábbi alkalmazáskönyvtárak jogosultságát:", "MySQL is used as database but does not support 4-byte characters. To be able to handle 4-byte characters (like emojis) without issues in filenames or comments for example it is recommended to enable the 4-byte support in MySQL. For further details read {linkstart}the documentation page about this ↗{linkend}." : "A MySQL adatbázis van használatban, de nem támogatja a 4 bájtos karaktereket. Hogy a 4 bájtos karakterek (például az emodzsikat) problémák nélkül kezelhetők legyenek, például a fájlnevekben vagy a megjegyzésekben, ajánlott engedélyezni a 4 bájtos támogatást a MySQL-ben. További részletekért olvassa el a {linkstart}erről szóló dokumentációs oldalt ↗{linkend}.", diff --git a/core/l10n/pl.js b/core/l10n/pl.js index 8cdb3531a64..9cec8fcb8c8 100644 --- a/core/l10n/pl.js +++ b/core/l10n/pl.js @@ -100,8 +100,10 @@ OC.L10N.register( "The PHP module \"imagick\" is not enabled although the theming app is. For favicon generation to work correctly, you need to install and enable this module." : "Moduł PHP \"imagick\" nie jest włączony, pomimo że aplikacja motywu jest. Aby generowanie favicon działało poprawnie, musisz zainstalować i włączyć ten moduł.", "The PHP modules \"gmp\" and/or \"bcmath\" are not enabled. If you use WebAuthn passwordless authentication, these modules are required." : "Moduły PHP \"gmp\" i/lub \"bcmath\" nie są włączone. Jeśli używasz uwierzytelniania WebAuthn bez hasła, te moduły są wymagane.", "Module php-imagick in this instance has no SVG support. For better compatibility it is recommended to install it." : "Moduł php-imagick w tej instancji nie obsługuje formatu SVG. Aby uzyskać lepszą kompatybilność, zaleca się jego zainstalowanie.", + "Some columns in the database are missing a conversion to big int. Due to the fact that changing column types on big tables could take some time they were not changed automatically. By running \"occ db:convert-filecache-bigint\" those pending changes could be applied manually. This operation needs to be made while the instance is offline. For further details read {linkstart}the documentation page about this ↗{linkend}." : "Niektóre kolumny w bazie danych nie zawierają konwersji do big integers. Ze względu na to, że zmiana typów kolumn w dużych tabelach może zająć dużo czasu, nie zostały one zmienione automatycznie. Oczekujące zmiany można wykonać ręcznie, uruchamiając \"occ db:convert-filecache-bigint\". Ta operacja musi zostać wykonana, gdy instancja jest w trybie offline. Więcej informacji na ten temat przeczytasz na {linkstart}stronie dokumentacji ↗{linkend}.", "SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend." : "SQLite jest aktualnie używany jako baza danych. Dla większych instalacji zalecamy przełączenie na inną bazę danych.", "This is particularly recommended when using the desktop client for file synchronisation." : "Jest to szczególnie zalecane podczas używania klienta desktopowego do synchronizacji plików.", + "To migrate to another database use the command line tool: \"occ db:convert-type\", or see the {linkstart}documentation ↗{linkend}." : "Aby przeprowadzić migrację do innej bazy danych, użyj narzędzia wiersza poleceń: \"occ db:convert-type\" lub zapoznaj się z {linkstart}dokumentacją ↗{linkend}.", "The PHP memory limit is below the recommended value of 512MB." : "Limit pamięci PHP jest poniżej zalecanej wartości 512MB.", "Some app directories are owned by a different user than the web server one. This may be the case if apps have been installed manually. Check the permissions of the following app directories:" : "Niektóre katalogi aplikacji są własnością innego użytkownika tego serwera WWW. Może to wystąpić, gdy aplikacje zostały zainstalowane ręcznie. Sprawdź uprawnienia poniższych katalogów:", "MySQL is used as database but does not support 4-byte characters. To be able to handle 4-byte characters (like emojis) without issues in filenames or comments for example it is recommended to enable the 4-byte support in MySQL. For further details read {linkstart}the documentation page about this ↗{linkend}." : "MySQL jest używany jako baza danych, ale nie obsługuje znaków 4-bajtowych. Aby korzystać ze znaków 4-bajtowych w nazwach plików lub komentarzach (np. Emoji), zaleca się włączenie tej obsługi w MySQL. Więcej informacji na ten temat przeczytasz na {linkstart}stronie dokumentacji ↗{linkend}.", diff --git a/core/l10n/pl.json b/core/l10n/pl.json index bf8688bcbce..b0f75e0c05f 100644 --- a/core/l10n/pl.json +++ b/core/l10n/pl.json @@ -98,8 +98,10 @@ "The PHP module \"imagick\" is not enabled although the theming app is. For favicon generation to work correctly, you need to install and enable this module." : "Moduł PHP \"imagick\" nie jest włączony, pomimo że aplikacja motywu jest. Aby generowanie favicon działało poprawnie, musisz zainstalować i włączyć ten moduł.", "The PHP modules \"gmp\" and/or \"bcmath\" are not enabled. If you use WebAuthn passwordless authentication, these modules are required." : "Moduły PHP \"gmp\" i/lub \"bcmath\" nie są włączone. Jeśli używasz uwierzytelniania WebAuthn bez hasła, te moduły są wymagane.", "Module php-imagick in this instance has no SVG support. For better compatibility it is recommended to install it." : "Moduł php-imagick w tej instancji nie obsługuje formatu SVG. Aby uzyskać lepszą kompatybilność, zaleca się jego zainstalowanie.", + "Some columns in the database are missing a conversion to big int. Due to the fact that changing column types on big tables could take some time they were not changed automatically. By running \"occ db:convert-filecache-bigint\" those pending changes could be applied manually. This operation needs to be made while the instance is offline. For further details read {linkstart}the documentation page about this ↗{linkend}." : "Niektóre kolumny w bazie danych nie zawierają konwersji do big integers. Ze względu na to, że zmiana typów kolumn w dużych tabelach może zająć dużo czasu, nie zostały one zmienione automatycznie. Oczekujące zmiany można wykonać ręcznie, uruchamiając \"occ db:convert-filecache-bigint\". Ta operacja musi zostać wykonana, gdy instancja jest w trybie offline. Więcej informacji na ten temat przeczytasz na {linkstart}stronie dokumentacji ↗{linkend}.", "SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend." : "SQLite jest aktualnie używany jako baza danych. Dla większych instalacji zalecamy przełączenie na inną bazę danych.", "This is particularly recommended when using the desktop client for file synchronisation." : "Jest to szczególnie zalecane podczas używania klienta desktopowego do synchronizacji plików.", + "To migrate to another database use the command line tool: \"occ db:convert-type\", or see the {linkstart}documentation ↗{linkend}." : "Aby przeprowadzić migrację do innej bazy danych, użyj narzędzia wiersza poleceń: \"occ db:convert-type\" lub zapoznaj się z {linkstart}dokumentacją ↗{linkend}.", "The PHP memory limit is below the recommended value of 512MB." : "Limit pamięci PHP jest poniżej zalecanej wartości 512MB.", "Some app directories are owned by a different user than the web server one. This may be the case if apps have been installed manually. Check the permissions of the following app directories:" : "Niektóre katalogi aplikacji są własnością innego użytkownika tego serwera WWW. Może to wystąpić, gdy aplikacje zostały zainstalowane ręcznie. Sprawdź uprawnienia poniższych katalogów:", "MySQL is used as database but does not support 4-byte characters. To be able to handle 4-byte characters (like emojis) without issues in filenames or comments for example it is recommended to enable the 4-byte support in MySQL. For further details read {linkstart}the documentation page about this ↗{linkend}." : "MySQL jest używany jako baza danych, ale nie obsługuje znaków 4-bajtowych. Aby korzystać ze znaków 4-bajtowych w nazwach plików lub komentarzach (np. Emoji), zaleca się włączenie tej obsługi w MySQL. Więcej informacji na ten temat przeczytasz na {linkstart}stronie dokumentacji ↗{linkend}.", diff --git a/core/l10n/pt_BR.js b/core/l10n/pt_BR.js index 825e11d27cb..294dcd6967a 100644 --- a/core/l10n/pt_BR.js +++ b/core/l10n/pt_BR.js @@ -86,6 +86,7 @@ OC.L10N.register( "The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy. If not, this is a security issue and can allow an attacker to spoof their IP address as visible to the Nextcloud. Further information can be found in the {linkstart}documentation ↗{linkend}." : "A configuração do cabeçalho do proxy reverso está incorreta ou você está acessando o Nextcloud de um proxy confiável. Caso contrário, este é um problema de segurança e pode permitir que um invasor falsifique seu endereço IP como visível para o Nextcloud. Mais informações podem ser encontradas na {linkstart}documentação ↗{linkend}. ", "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 {linkstart}memcached wiki about both modules ↗{linkend}." : "Memcached está configurado como cache distribuído, mas a extensão PHP incorreta \"memcache\" está instalada. \\OC\\Memcache\\Memcached suporta apenas \"memcached\" e não \"memcache\". Veja o {linkstart}memcached wiki sobre ambas as extensões ↗{linkend}.", "Some files have not passed the integrity check. Further information on how to resolve this issue can be found in the {linkstart1}documentation ↗{linkend}. ({linkstart2}List of invalid files…{linkend} / {linkstart3}Rescan…{linkend})" : "Alguns arquivos não passaram na verificação de integridade. Mais informações sobre como resolver esse problema podem ser encontradas na {linkstart1}documentação ↗{linkend}. ({linkstart2}Lista de arquivos inválidos…{linkend} / {linkstart3}Verificar novamente…{linkend})", + "The PHP OPcache module is not properly configured. See the {linkstart}documentation ↗{linkend} for more information." : "O módulo PHP OPcache não está configurado corretamente. Veja a{linkstart}documentação ↗{linkend} para mais informações.", "The PHP function \"set_time_limit\" is not available. This could result in scripts being halted mid-execution, breaking your installation. Enabling this function is strongly recommended." : "A função PHP \"set_time_limit\" não está disponível. Isso pode resultar em travamento de scripts, quebrando sua instalação. A ativação desta função é altamente recomendada.", "Your PHP does not have FreeType support, resulting in breakage of profile pictures and the settings interface." : "Seu PHP não possui suporte à FreeType, resultando em problemas nas fotos de perfil e interface de configurações.", "Missing index \"{indexName}\" in table \"{tableName}\"." : "Falta o índice \"{indexName}\" na tabela \"{tableName}\".", @@ -95,6 +96,8 @@ OC.L10N.register( "Missing optional column \"{columnName}\" in table \"{tableName}\"." : "Falta a coluna opcional \"{columnName}\" na tabela \"{tableName}\".", "The database is missing some optional columns. Due to the fact that adding columns on big tables could take some time they were not added automatically when they can be optional. By running \"occ db:add-missing-columns\" those missing columns could be added manually while the instance keeps running. Once the columns are added some features might improve responsiveness or usability." : "Estão faltando algumas colunas opcionais no banco de dados. Devido ao fato de que adicionar colunas em grandes tabelas pode levar algum tempo, elas não foram adicionadas automaticamente por serem opcionais. Ao executar \"occ db: add-missing-columns\", elas podem ser adicionadas manualmente enquanto a instância continua em execução. Depois que as colunas são adicionadas, alguns recursos podem melhorar a capacidade de resposta ou a usabilidade.", "This instance is missing some recommended PHP modules. For improved performance and better compatibility it is highly recommended to install them." : "Nesta instalação estão faltando alguns módulos PHP recomendados. Para melhor desempenho e compatibilidade, é altamente recomendável instalá-los.", + "The PHP module \"imagick\" is not enabled although the theming app is. For favicon generation to work correctly, you need to install and enable this module." : "O módulo PHP \"imagick\" não está habilitado, embora o aplicativo de temas esteja. Para que a geração de favicon funcione corretamente, você precisa instalar e habilitar este módulo.", + "The PHP modules \"gmp\" and/or \"bcmath\" are not enabled. If you use WebAuthn passwordless authentication, these modules are required." : "Os módulos PHP \"gmp\" e/ou \"bcmath\" não estão habilitados. Se você usar a autenticação sem senha do WebAuthn, esses módulos serão necessários.", "Module php-imagick in this instance has no SVG support. For better compatibility it is recommended to install it." : "A extensão php-imagick nesta instância não tem suporte para SVG. Para melhor compatibilidade é recomendado instalá-la.", "SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend." : "Atualmente, o SQLite está sendo usado como plataforma de banco de dados. Para instalações maiores, recomendamos que migre para outra.", "This is particularly recommended when using the desktop client for file synchronisation." : "Isso é recomendado ao usar o cliente da área de trabalho para sincronização de arquivos.", diff --git a/core/l10n/pt_BR.json b/core/l10n/pt_BR.json index e35c18b01a8..94b03d87929 100644 --- a/core/l10n/pt_BR.json +++ b/core/l10n/pt_BR.json @@ -84,6 +84,7 @@ "The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy. If not, this is a security issue and can allow an attacker to spoof their IP address as visible to the Nextcloud. Further information can be found in the {linkstart}documentation ↗{linkend}." : "A configuração do cabeçalho do proxy reverso está incorreta ou você está acessando o Nextcloud de um proxy confiável. Caso contrário, este é um problema de segurança e pode permitir que um invasor falsifique seu endereço IP como visível para o Nextcloud. Mais informações podem ser encontradas na {linkstart}documentação ↗{linkend}. ", "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 {linkstart}memcached wiki about both modules ↗{linkend}." : "Memcached está configurado como cache distribuído, mas a extensão PHP incorreta \"memcache\" está instalada. \\OC\\Memcache\\Memcached suporta apenas \"memcached\" e não \"memcache\". Veja o {linkstart}memcached wiki sobre ambas as extensões ↗{linkend}.", "Some files have not passed the integrity check. Further information on how to resolve this issue can be found in the {linkstart1}documentation ↗{linkend}. ({linkstart2}List of invalid files…{linkend} / {linkstart3}Rescan…{linkend})" : "Alguns arquivos não passaram na verificação de integridade. Mais informações sobre como resolver esse problema podem ser encontradas na {linkstart1}documentação ↗{linkend}. ({linkstart2}Lista de arquivos inválidos…{linkend} / {linkstart3}Verificar novamente…{linkend})", + "The PHP OPcache module is not properly configured. See the {linkstart}documentation ↗{linkend} for more information." : "O módulo PHP OPcache não está configurado corretamente. Veja a{linkstart}documentação ↗{linkend} para mais informações.", "The PHP function \"set_time_limit\" is not available. This could result in scripts being halted mid-execution, breaking your installation. Enabling this function is strongly recommended." : "A função PHP \"set_time_limit\" não está disponível. Isso pode resultar em travamento de scripts, quebrando sua instalação. A ativação desta função é altamente recomendada.", "Your PHP does not have FreeType support, resulting in breakage of profile pictures and the settings interface." : "Seu PHP não possui suporte à FreeType, resultando em problemas nas fotos de perfil e interface de configurações.", "Missing index \"{indexName}\" in table \"{tableName}\"." : "Falta o índice \"{indexName}\" na tabela \"{tableName}\".", @@ -93,6 +94,8 @@ "Missing optional column \"{columnName}\" in table \"{tableName}\"." : "Falta a coluna opcional \"{columnName}\" na tabela \"{tableName}\".", "The database is missing some optional columns. Due to the fact that adding columns on big tables could take some time they were not added automatically when they can be optional. By running \"occ db:add-missing-columns\" those missing columns could be added manually while the instance keeps running. Once the columns are added some features might improve responsiveness or usability." : "Estão faltando algumas colunas opcionais no banco de dados. Devido ao fato de que adicionar colunas em grandes tabelas pode levar algum tempo, elas não foram adicionadas automaticamente por serem opcionais. Ao executar \"occ db: add-missing-columns\", elas podem ser adicionadas manualmente enquanto a instância continua em execução. Depois que as colunas são adicionadas, alguns recursos podem melhorar a capacidade de resposta ou a usabilidade.", "This instance is missing some recommended PHP modules. For improved performance and better compatibility it is highly recommended to install them." : "Nesta instalação estão faltando alguns módulos PHP recomendados. Para melhor desempenho e compatibilidade, é altamente recomendável instalá-los.", + "The PHP module \"imagick\" is not enabled although the theming app is. For favicon generation to work correctly, you need to install and enable this module." : "O módulo PHP \"imagick\" não está habilitado, embora o aplicativo de temas esteja. Para que a geração de favicon funcione corretamente, você precisa instalar e habilitar este módulo.", + "The PHP modules \"gmp\" and/or \"bcmath\" are not enabled. If you use WebAuthn passwordless authentication, these modules are required." : "Os módulos PHP \"gmp\" e/ou \"bcmath\" não estão habilitados. Se você usar a autenticação sem senha do WebAuthn, esses módulos serão necessários.", "Module php-imagick in this instance has no SVG support. For better compatibility it is recommended to install it." : "A extensão php-imagick nesta instância não tem suporte para SVG. Para melhor compatibilidade é recomendado instalá-la.", "SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend." : "Atualmente, o SQLite está sendo usado como plataforma de banco de dados. Para instalações maiores, recomendamos que migre para outra.", "This is particularly recommended when using the desktop client for file synchronisation." : "Isso é recomendado ao usar o cliente da área de trabalho para sincronização de arquivos.", diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index aa6ff416ba1..ce316b882e6 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -202,6 +202,6 @@ $getUserAvatar = static function (int $size) use ($_): string { <div id="content" class="app-<?php p($_['appid']) ?>" role="main"> <?php print_unescaped($_['content']); ?> </div> - + <div id="profiler-toolbar"></div> </body> </html> diff --git a/lib/autoloader.php b/lib/autoloader.php index c8eebee3e0c..a29b9aece79 100644 --- a/lib/autoloader.php +++ b/lib/autoloader.php @@ -38,6 +38,7 @@ namespace OC; use \OCP\AutoloadNotAllowedException; use OCP\ILogger; +use OCP\ICache; class Autoloader { /** @var bool */ @@ -182,9 +183,9 @@ class Autoloader { /** * Sets the optional low-latency cache for class to path mapping. * - * @param \OC\Memcache\Cache $memoryCache Instance of memory cache. + * @param ICache $memoryCache Instance of memory cache. */ - public function setMemoryCache(\OC\Memcache\Cache $memoryCache = null): void { + public function setMemoryCache(ICache $memoryCache = null): void { $this->memoryCache = $memoryCache; } } diff --git a/lib/base.php b/lib/base.php index f3c3e4f31cb..21889272dd7 100644 --- a/lib/base.php +++ b/lib/base.php @@ -608,6 +608,7 @@ class OC { $eventLogger->end('request'); }); $eventLogger->start('boot', 'Initialize'); + $eventLogger->start('runtime', 'Runtime (total - autoloader)'); // Override php.ini and log everything if we're troubleshooting if (self::$config->getValue('loglevel') === ILogger::DEBUG) { diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 4dbb3cc0d05..2df13618053 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -196,6 +196,8 @@ return array( 'OCP\\Dashboard\\RegisterWidgetEvent' => $baseDir . '/lib/public/Dashboard/RegisterWidgetEvent.php', 'OCP\\Dashboard\\Service\\IEventsService' => $baseDir . '/lib/public/Dashboard/Service/IEventsService.php', 'OCP\\Dashboard\\Service\\IWidgetsService' => $baseDir . '/lib/public/Dashboard/Service/IWidgetsService.php', + 'OCP\\DataCollector\\AbstractDataCollector' => $baseDir . '/lib/public/DataCollector/AbstractDataCollector.php', + 'OCP\\DataCollector\\IDataCollector' => $baseDir . '/lib/public/DataCollector/IDataCollector.php', 'OCP\\Defaults' => $baseDir . '/lib/public/Defaults.php', 'OCP\\Diagnostics\\IEvent' => $baseDir . '/lib/public/Diagnostics/IEvent.php', 'OCP\\Diagnostics\\IEventLogger' => $baseDir . '/lib/public/Diagnostics/IEventLogger.php', @@ -467,6 +469,8 @@ return array( 'OCP\\Preview\\IVersionedPreviewFile' => $baseDir . '/lib/public/Preview/IVersionedPreviewFile.php', 'OCP\\Profile\\ILinkAction' => $baseDir . '/lib/public/Profile/ILinkAction.php', 'OCP\\Profile\\ParameterDoesNotExistException' => $baseDir . '/lib/public/Profile/ParameterDoesNotExistException.php', + 'OCP\\Profiler\\IProfile' => $baseDir . '/lib/public/Profiler/IProfile.php', + 'OCP\\Profiler\\IProfiler' => $baseDir . '/lib/public/Profiler/IProfiler.php', 'OCP\\Remote\\Api\\IApiCollection' => $baseDir . '/lib/public/Remote/Api/IApiCollection.php', 'OCP\\Remote\\Api\\IApiFactory' => $baseDir . '/lib/public/Remote/Api/IApiFactory.php', 'OCP\\Remote\\Api\\ICapabilitiesApi' => $baseDir . '/lib/public/Remote/Api/ICapabilitiesApi.php', @@ -1025,6 +1029,7 @@ return array( 'OC\\DB\\Connection' => $baseDir . '/lib/private/DB/Connection.php', 'OC\\DB\\ConnectionAdapter' => $baseDir . '/lib/private/DB/ConnectionAdapter.php', 'OC\\DB\\ConnectionFactory' => $baseDir . '/lib/private/DB/ConnectionFactory.php', + 'OC\\DB\\DbDataCollector' => $baseDir . '/lib/private/DB/DbDataCollector.php', 'OC\\DB\\Exceptions\\DbalException' => $baseDir . '/lib/private/DB/Exceptions/DbalException.php', 'OC\\DB\\MigrationException' => $baseDir . '/lib/private/DB/MigrationException.php', 'OC\\DB\\MigrationService' => $baseDir . '/lib/private/DB/MigrationService.php', @@ -1035,6 +1040,7 @@ return array( 'OC\\DB\\MySQLMigrator' => $baseDir . '/lib/private/DB/MySQLMigrator.php', 'OC\\DB\\MySqlTools' => $baseDir . '/lib/private/DB/MySqlTools.php', 'OC\\DB\\OCSqlitePlatform' => $baseDir . '/lib/private/DB/OCSqlitePlatform.php', + 'OC\\DB\\ObjectParameter' => $baseDir . '/lib/private/DB/ObjectParameter.php', 'OC\\DB\\OracleConnection' => $baseDir . '/lib/private/DB/OracleConnection.php', 'OC\\DB\\OracleMigrator' => $baseDir . '/lib/private/DB/OracleMigrator.php', 'OC\\DB\\PgSqlTools' => $baseDir . '/lib/private/DB/PgSqlTools.php', @@ -1279,8 +1285,10 @@ return array( 'OC\\Memcache\\CASTrait' => $baseDir . '/lib/private/Memcache/CASTrait.php', 'OC\\Memcache\\Cache' => $baseDir . '/lib/private/Memcache/Cache.php', 'OC\\Memcache\\Factory' => $baseDir . '/lib/private/Memcache/Factory.php', + 'OC\\Memcache\\LoggerWrapperCache' => $baseDir . '/lib/private/Memcache/LoggerWrapperCache.php', 'OC\\Memcache\\Memcached' => $baseDir . '/lib/private/Memcache/Memcached.php', 'OC\\Memcache\\NullCache' => $baseDir . '/lib/private/Memcache/NullCache.php', + 'OC\\Memcache\\ProfilerWrapperCache' => $baseDir . '/lib/private/Memcache/ProfilerWrapperCache.php', 'OC\\Memcache\\Redis' => $baseDir . '/lib/private/Memcache/Redis.php', 'OC\\MemoryInfo' => $baseDir . '/lib/private/MemoryInfo.php', 'OC\\Migration\\BackgroundRepair' => $baseDir . '/lib/private/Migration/BackgroundRepair.php', @@ -1347,6 +1355,10 @@ return array( 'OC\\Profile\\Actions\\WebsiteAction' => $baseDir . '/lib/private/Profile/Actions/WebsiteAction.php', 'OC\\Profile\\ProfileManager' => $baseDir . '/lib/private/Profile/ProfileManager.php', 'OC\\Profile\\TProfileHelper' => $baseDir . '/lib/private/Profile/TProfileHelper.php', + 'OC\\Profiler\\FileProfilerStorage' => $baseDir . '/lib/private/Profiler/FileProfilerStorage.php', + 'OC\\Profiler\\Profile' => $baseDir . '/lib/private/Profiler/Profile.php', + 'OC\\Profiler\\Profiler' => $baseDir . '/lib/private/Profiler/Profiler.php', + 'OC\\Profiler\\RoutingDataCollector' => $baseDir . '/lib/private/Profiler/RoutingDataCollector.php', 'OC\\RedisFactory' => $baseDir . '/lib/private/RedisFactory.php', 'OC\\Remote\\Api\\ApiBase' => $baseDir . '/lib/private/Remote/Api/ApiBase.php', 'OC\\Remote\\Api\\ApiCollection' => $baseDir . '/lib/private/Remote/Api/ApiCollection.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 0b64f70f6fd..cd5d30b3574 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -225,6 +225,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Dashboard\\RegisterWidgetEvent' => __DIR__ . '/../../..' . '/lib/public/Dashboard/RegisterWidgetEvent.php', 'OCP\\Dashboard\\Service\\IEventsService' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Service/IEventsService.php', 'OCP\\Dashboard\\Service\\IWidgetsService' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Service/IWidgetsService.php', + 'OCP\\DataCollector\\AbstractDataCollector' => __DIR__ . '/../../..' . '/lib/public/DataCollector/AbstractDataCollector.php', + 'OCP\\DataCollector\\IDataCollector' => __DIR__ . '/../../..' . '/lib/public/DataCollector/IDataCollector.php', 'OCP\\Defaults' => __DIR__ . '/../../..' . '/lib/public/Defaults.php', 'OCP\\Diagnostics\\IEvent' => __DIR__ . '/../../..' . '/lib/public/Diagnostics/IEvent.php', 'OCP\\Diagnostics\\IEventLogger' => __DIR__ . '/../../..' . '/lib/public/Diagnostics/IEventLogger.php', @@ -496,6 +498,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Preview\\IVersionedPreviewFile' => __DIR__ . '/../../..' . '/lib/public/Preview/IVersionedPreviewFile.php', 'OCP\\Profile\\ILinkAction' => __DIR__ . '/../../..' . '/lib/public/Profile/ILinkAction.php', 'OCP\\Profile\\ParameterDoesNotExistException' => __DIR__ . '/../../..' . '/lib/public/Profile/ParameterDoesNotExistException.php', + 'OCP\\Profiler\\IProfile' => __DIR__ . '/../../..' . '/lib/public/Profiler/IProfile.php', + 'OCP\\Profiler\\IProfiler' => __DIR__ . '/../../..' . '/lib/public/Profiler/IProfiler.php', 'OCP\\Remote\\Api\\IApiCollection' => __DIR__ . '/../../..' . '/lib/public/Remote/Api/IApiCollection.php', 'OCP\\Remote\\Api\\IApiFactory' => __DIR__ . '/../../..' . '/lib/public/Remote/Api/IApiFactory.php', 'OCP\\Remote\\Api\\ICapabilitiesApi' => __DIR__ . '/../../..' . '/lib/public/Remote/Api/ICapabilitiesApi.php', @@ -1054,6 +1058,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\DB\\Connection' => __DIR__ . '/../../..' . '/lib/private/DB/Connection.php', 'OC\\DB\\ConnectionAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ConnectionAdapter.php', 'OC\\DB\\ConnectionFactory' => __DIR__ . '/../../..' . '/lib/private/DB/ConnectionFactory.php', + 'OC\\DB\\DbDataCollector' => __DIR__ . '/../../..' . '/lib/private/DB/DbDataCollector.php', 'OC\\DB\\Exceptions\\DbalException' => __DIR__ . '/../../..' . '/lib/private/DB/Exceptions/DbalException.php', 'OC\\DB\\MigrationException' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationException.php', 'OC\\DB\\MigrationService' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationService.php', @@ -1064,6 +1069,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\DB\\MySQLMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/MySQLMigrator.php', 'OC\\DB\\MySqlTools' => __DIR__ . '/../../..' . '/lib/private/DB/MySqlTools.php', 'OC\\DB\\OCSqlitePlatform' => __DIR__ . '/../../..' . '/lib/private/DB/OCSqlitePlatform.php', + 'OC\\DB\\ObjectParameter' => __DIR__ . '/../../..' . '/lib/private/DB/ObjectParameter.php', 'OC\\DB\\OracleConnection' => __DIR__ . '/../../..' . '/lib/private/DB/OracleConnection.php', 'OC\\DB\\OracleMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/OracleMigrator.php', 'OC\\DB\\PgSqlTools' => __DIR__ . '/../../..' . '/lib/private/DB/PgSqlTools.php', @@ -1308,8 +1314,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Memcache\\CASTrait' => __DIR__ . '/../../..' . '/lib/private/Memcache/CASTrait.php', 'OC\\Memcache\\Cache' => __DIR__ . '/../../..' . '/lib/private/Memcache/Cache.php', 'OC\\Memcache\\Factory' => __DIR__ . '/../../..' . '/lib/private/Memcache/Factory.php', + 'OC\\Memcache\\LoggerWrapperCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/LoggerWrapperCache.php', 'OC\\Memcache\\Memcached' => __DIR__ . '/../../..' . '/lib/private/Memcache/Memcached.php', 'OC\\Memcache\\NullCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/NullCache.php', + 'OC\\Memcache\\ProfilerWrapperCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/ProfilerWrapperCache.php', 'OC\\Memcache\\Redis' => __DIR__ . '/../../..' . '/lib/private/Memcache/Redis.php', 'OC\\MemoryInfo' => __DIR__ . '/../../..' . '/lib/private/MemoryInfo.php', 'OC\\Migration\\BackgroundRepair' => __DIR__ . '/../../..' . '/lib/private/Migration/BackgroundRepair.php', @@ -1376,6 +1384,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Profile\\Actions\\WebsiteAction' => __DIR__ . '/../../..' . '/lib/private/Profile/Actions/WebsiteAction.php', 'OC\\Profile\\ProfileManager' => __DIR__ . '/../../..' . '/lib/private/Profile/ProfileManager.php', 'OC\\Profile\\TProfileHelper' => __DIR__ . '/../../..' . '/lib/private/Profile/TProfileHelper.php', + 'OC\\Profiler\\FileProfilerStorage' => __DIR__ . '/../../..' . '/lib/private/Profiler/FileProfilerStorage.php', + 'OC\\Profiler\\Profile' => __DIR__ . '/../../..' . '/lib/private/Profiler/Profile.php', + 'OC\\Profiler\\Profiler' => __DIR__ . '/../../..' . '/lib/private/Profiler/Profiler.php', + 'OC\\Profiler\\RoutingDataCollector' => __DIR__ . '/../../..' . '/lib/private/Profiler/RoutingDataCollector.php', 'OC\\RedisFactory' => __DIR__ . '/../../..' . '/lib/private/RedisFactory.php', 'OC\\Remote\\Api\\ApiBase' => __DIR__ . '/../../..' . '/lib/private/Remote/Api/ApiBase.php', 'OC\\Remote\\Api\\ApiCollection' => __DIR__ . '/../../..' . '/lib/private/Remote/Api/ApiCollection.php', diff --git a/lib/l10n/fr.js b/lib/l10n/fr.js index 43a9d004886..a373d54db51 100644 --- a/lib/l10n/fr.js +++ b/lib/l10n/fr.js @@ -274,4 +274,4 @@ OC.L10N.register( "Your data directory is invalid" : "Votre répertoire n'est pas valide", "This can usually be fixed by giving the webserver write access to the apps directory or disabling the App Store in the config file." : "Ce problème est généralement résolu en donnant au serveur web un accès en écriture au répertoire des applications ou en désactivant le magasin d'applications dans le fichier de configuration." }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n==0 || n==1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/lib/l10n/fr.json b/lib/l10n/fr.json index 8b31eccae6f..0a8b3bbc264 100644 --- a/lib/l10n/fr.json +++ b/lib/l10n/fr.json @@ -271,5 +271,5 @@ "Check the value of \"datadirectory\" in your configuration" : "Verifiez la valeur de \"datadirectory\" dans votre configuration", "Your data directory is invalid" : "Votre répertoire n'est pas valide", "This can usually be fixed by giving the webserver write access to the apps directory or disabling the App Store in the config file." : "Ce problème est généralement résolu en donnant au serveur web un accès en écriture au répertoire des applications ou en désactivant le magasin d'applications dans le fichier de configuration." -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"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/lib/private/AppFramework/App.php b/lib/private/AppFramework/App.php index 6c2f905afa5..feebb32d5bc 100644 --- a/lib/private/AppFramework/App.php +++ b/lib/private/AppFramework/App.php @@ -34,11 +34,16 @@ namespace OC\AppFramework; use OC\AppFramework\DependencyInjection\DIContainer; use OC\AppFramework\Http\Dispatcher; use OC\AppFramework\Http\Request; +use OC\Diagnostics\EventLogger; +use OCP\Profiler\IProfiler; +use OC\Profiler\RoutingDataCollector; +use OCP\AppFramework\QueryException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\ICallbackResponse; use OCP\AppFramework\Http\IOutput; -use OCP\AppFramework\QueryException; +use OCP\Diagnostics\IEventLogger; use OCP\HintException; +use OCP\IConfig; use OCP\IRequest; /** @@ -114,20 +119,30 @@ class App { * @throws HintException */ public static function main(string $controllerName, string $methodName, DIContainer $container, array $urlParams = null) { + /** @var IProfiler $profiler */ + $profiler = $container->get(IProfiler::class); + $config = $container->get(IConfig::class); + // Disable profiler on the profiler UI + $profiler->setEnabled($profiler->isEnabled() && !is_null($urlParams) && isset($urlParams['_route']) && !str_starts_with($urlParams['_route'], 'profiler.')); + if ($profiler->isEnabled()) { + \OC::$server->get(IEventLogger::class)->activate(); + $profiler->add(new RoutingDataCollector($container['AppName'], $controllerName, $methodName)); + } + if (!is_null($urlParams)) { /** @var Request $request */ - $request = $container->query(IRequest::class); + $request = $container->get(IRequest::class); $request->setUrlParameters($urlParams); } elseif (isset($container['urlParams']) && !is_null($container['urlParams'])) { /** @var Request $request */ - $request = $container->query(IRequest::class); + $request = $container->get(IRequest::class); $request->setUrlParameters($container['urlParams']); } $appName = $container['AppName']; // first try $controllerName then go for \OCA\AppName\Controller\$controllerName try { - $controller = $container->query($controllerName); + $controller = $container->get($controllerName); } catch (QueryException $e) { if (strpos($controllerName, '\\Controller\\') !== false) { // This is from a global registered app route that is not enabled. @@ -158,6 +173,16 @@ class App { $io = $container[IOutput::class]; + if ($profiler->isEnabled()) { + /** @var EventLogger $eventLogger */ + $eventLogger = $container->get(IEventLogger::class); + $eventLogger->end('runtime'); + $profile = $profiler->collect($container->get(IRequest::class), $response); + $profiler->saveProfile($profile); + $io->setHeader('X-Debug-Token:' . $profile->getToken()); + $io->setHeader('Server-Timing: token;desc="' . $profile->getToken() . '"'); + } + if (!is_null($httpHeaders)) { $io->setHeader($httpHeaders); } diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index 598c66b6aba..429382aa223 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -38,6 +38,7 @@ use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionException; use ReflectionParameter; +use ReflectionNamedType; use function class_exists; /** @@ -78,12 +79,13 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { $resolveName = $parameter->getName(); // try to find out if it is a class or a simple parameter - if ($parameterType !== null && !$parameterType->isBuiltin()) { + if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) { $resolveName = $parameterType->getName(); } try { - $builtIn = $parameter->hasType() && $parameter->getType()->isBuiltin(); + $builtIn = $parameter->hasType() && ($parameter->getType() instanceof ReflectionNamedType) + && $parameter->getType()->isBuiltin(); return $this->query($resolveName, !$builtIn); } catch (QueryException $e) { // Service not found, use the default value when available @@ -91,7 +93,7 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { return $parameter->getDefaultValue(); } - if ($parameterType !== null && !$parameterType->isBuiltin()) { + if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) { $resolveName = $parameter->getName(); try { return $this->query($resolveName); diff --git a/lib/private/Cache/CappedMemoryCache.php b/lib/private/Cache/CappedMemoryCache.php index 9260bf1f6b3..0a3300435eb 100644 --- a/lib/private/Cache/CappedMemoryCache.php +++ b/lib/private/Cache/CappedMemoryCache.php @@ -115,4 +115,8 @@ class CappedMemoryCache implements ICache, \ArrayAccess { $this->remove($key); } } + + public static function isAvailable(): bool { + return true; + } } diff --git a/lib/private/Cache/File.php b/lib/private/Cache/File.php index 0ecd894d2d2..a96a7cd9c0b 100644 --- a/lib/private/Cache/File.php +++ b/lib/private/Cache/File.php @@ -203,4 +203,8 @@ class File implements ICache { } } } + + public static function isAvailable(): bool { + return true; + } } diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 0cd310550b6..2e38b1ddf5e 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -42,6 +42,7 @@ use Doctrine\DBAL\Driver; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\ConstraintViolationException; use Doctrine\DBAL\Exception\NotNullConstraintViolationException; +use Doctrine\DBAL\Logging\DebugStack; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Platforms\PostgreSQL94Platform; @@ -55,6 +56,7 @@ use OCP\PreConditionNotMetException; use OC\DB\QueryBuilder\QueryBuilder; use OC\SystemConfig; use Psr\Log\LoggerInterface; +use OCP\Profiler\IProfiler; class Connection extends \Doctrine\DBAL\Connection { /** @var string */ @@ -76,6 +78,9 @@ class Connection extends \Doctrine\DBAL\Connection { /** @var int */ protected $queriesExecuted = 0; + /** @var DbDataCollector|null */ + protected $dbDataCollector = null; + /** * Initializes a new instance of the Connection class. * @@ -102,6 +107,16 @@ class Connection extends \Doctrine\DBAL\Connection { $this->systemConfig = \OC::$server->getSystemConfig(); $this->logger = \OC::$server->get(LoggerInterface::class); + + /** @var \OCP\Profiler\IProfiler */ + $profiler = \OC::$server->get(IProfiler::class); + if ($profiler->isEnabled()) { + $this->dbDataCollector = new DbDataCollector($this); + $profiler->add($this->dbDataCollector); + $debugStack = new DebugStack(); + $this->dbDataCollector->setDebugStack($debugStack); + $this->_config->setSQLLogger($debugStack); + } } /** diff --git a/lib/private/DB/DbDataCollector.php b/lib/private/DB/DbDataCollector.php new file mode 100644 index 00000000000..d708955b10e --- /dev/null +++ b/lib/private/DB/DbDataCollector.php @@ -0,0 +1,154 @@ +<?php + +declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Logging\DebugStack; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; + +class DbDataCollector extends \OCP\DataCollector\AbstractDataCollector { + protected ?DebugStack $debugStack = null; + private Connection $connection; + + /** + * DbDataCollector constructor. + */ + public function __construct(Connection $connection) { + $this->connection = $connection; + } + + public function setDebugStack(DebugStack $debugStack, $name = 'default'): void { + $this->debugStack = $debugStack; + } + + /** + * @inheritDoc + */ + public function collect(Request $request, Response $response, \Throwable $exception = null): void { + $queries = $this->sanitizeQueries($this->debugStack->queries); + + $this->data = [ + 'queries' => $queries, + ]; + } + + public function getName(): string { + return 'db'; + } + + public function getQueries(): array { + return $this->data['queries']; + } + + private function sanitizeQueries(array $queries): array { + foreach ($queries as $i => $query) { + $queries[$i] = $this->sanitizeQuery($query); + } + + return $queries; + } + + private function sanitizeQuery(array $query): array { + $query['explainable'] = true; + $query['runnable'] = true; + if (null === $query['params']) { + $query['params'] = []; + } + if (!\is_array($query['params'])) { + $query['params'] = [$query['params']]; + } + if (!\is_array($query['types'])) { + $query['types'] = []; + } + foreach ($query['params'] as $j => $param) { + $e = null; + if (isset($query['types'][$j])) { + // Transform the param according to the type + $type = $query['types'][$j]; + if (\is_string($type)) { + $type = Type::getType($type); + } + if ($type instanceof Type) { + $query['types'][$j] = $type->getBindingType(); + try { + $param = $type->convertToDatabaseValue($param, $this->connection->getDatabasePlatform()); + } catch (\TypeError $e) { + } catch (ConversionException $e) { + } + } + } + + [$query['params'][$j], $explainable, $runnable] = $this->sanitizeParam($param, $e); + if (!$explainable) { + $query['explainable'] = false; + } + + if (!$runnable) { + $query['runnable'] = false; + } + } + + return $query; + } + + /** + * Sanitizes a param. + * + * The return value is an array with the sanitized value and a boolean + * indicating if the original value was kept (allowing to use the sanitized + * value to explain the query). + */ + private function sanitizeParam($var, ?\Throwable $error): array { + if (\is_object($var)) { + return [$o = new ObjectParameter($var, $error), false, $o->isStringable() && !$error]; + } + + if ($error) { + return ['⚠ '.$error->getMessage(), false, false]; + } + + if (\is_array($var)) { + $a = []; + $explainable = $runnable = true; + foreach ($var as $k => $v) { + [$value, $e, $r] = $this->sanitizeParam($v, null); + $explainable = $explainable && $e; + $runnable = $runnable && $r; + $a[$k] = $value; + } + + return [$a, $explainable, $runnable]; + } + + if (\is_resource($var)) { + return [sprintf('/* Resource(%s) */', get_resource_type($var)), false, false]; + } + + return [$var, true, true]; + } +} diff --git a/lib/private/DB/ObjectParameter.php b/lib/private/DB/ObjectParameter.php new file mode 100644 index 00000000000..61ac16018d8 --- /dev/null +++ b/lib/private/DB/ObjectParameter.php @@ -0,0 +1,71 @@ +<?php + +declare(strict_types = 1); + +/* + * This file is part of the Symfony package. + * + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * @author Fabien Potencier <fabien@symfony.com> + * + * @license AGPL-3.0-or-later AND MIT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\DB; + +final class ObjectParameter { + private $object; + private $error; + private $stringable; + private $class; + + /** + * @param object $object + */ + public function __construct($object, ?\Throwable $error) { + $this->object = $object; + $this->error = $error; + $this->stringable = \is_callable([$object, '__toString']); + $this->class = \get_class($object); + } + + /** + * @return object + */ + public function getObject() { + return $this->object; + } + + public function getError(): ?\Throwable { + return $this->error; + } + + public function isStringable(): bool { + return $this->stringable; + } + + public function getClass(): string { + return $this->class; + } +} diff --git a/lib/private/Diagnostics/EventLogger.php b/lib/private/Diagnostics/EventLogger.php index 35cef0be3f5..c7b89002ea9 100644 --- a/lib/private/Diagnostics/EventLogger.php +++ b/lib/private/Diagnostics/EventLogger.php @@ -60,7 +60,8 @@ class EventLogger implements IEventLogger { } public function isLoggingActivated(): bool { - $systemValue = (bool)$this->config->getValue('diagnostics.logging', false); + $systemValue = (bool)$this->config->getValue('diagnostics.logging', false) + || (bool)$this->config->getValue('profiler', false); if ($systemValue && $this->config->getValue('debug', false)) { return true; diff --git a/lib/private/Files/Cache/Storage.php b/lib/private/Files/Cache/Storage.php index 2de2c2f84d7..fb9e5500658 100644 --- a/lib/private/Files/Cache/Storage.php +++ b/lib/private/Files/Cache/Storage.php @@ -126,19 +126,9 @@ class Storage { * @param int $numericId * @return string|null either the storage id string or null if the numeric id is not known */ - public static function getStorageId($numericId) { - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); - $query->select('id') - ->from('storages') - ->where($query->expr()->eq('numeric_id', $query->createNamedParameter($numericId))); - $result = $query->execute(); - $row = $result->fetch(); - $result->closeCursor(); - if ($row) { - return $row['id']; - } else { - return null; - } + public static function getStorageId(int $numericId): ?string { + $storage = self::getGlobalCache()->getStorageInfoByNumericId($numericId); + return $storage['id'] ?? null; } /** @@ -240,6 +230,7 @@ class Storage { ->from('mounts') ->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))); $storageIds = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + $storageIds = array_unique($storageIds); $query = $db->getQueryBuilder(); $query->delete('filecache') diff --git a/lib/private/Files/Cache/StorageGlobal.php b/lib/private/Files/Cache/StorageGlobal.php index 7162f8e4908..a898c435415 100644 --- a/lib/private/Files/Cache/StorageGlobal.php +++ b/lib/private/Files/Cache/StorageGlobal.php @@ -41,8 +41,10 @@ class StorageGlobal { /** @var IDBConnection */ private $connection; - /** @var array[] */ + /** @var array<string, array> */ private $cache = []; + /** @var array<int, array> */ + private $numericIdCache = []; public function __construct(IDBConnection $connection) { $this->connection = $connection; @@ -68,7 +70,7 @@ class StorageGlobal { * @param string $storageId * @return array|null */ - public function getStorageInfo($storageId) { + public function getStorageInfo(string $storageId): ?array { if (!isset($this->cache[$storageId])) { $builder = $this->connection->getQueryBuilder(); $query = $builder->select(['id', 'numeric_id', 'available', 'last_checked']) @@ -81,9 +83,33 @@ class StorageGlobal { if ($row) { $this->cache[$storageId] = $row; + $this->numericIdCache[(int)$row['numeric_id']] = $row; } } - return isset($this->cache[$storageId]) ? $this->cache[$storageId] : null; + return $this->cache[$storageId] ?? null; + } + + /** + * @param int $numericId + * @return array|null + */ + public function getStorageInfoByNumericId(int $numericId): ?array { + if (!isset($this->numericIdCache[$numericId])) { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->select(['id', 'numeric_id', 'available', 'last_checked']) + ->from('storages') + ->where($builder->expr()->eq('numeric_id', $builder->createNamedParameter($numericId))); + + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row) { + $this->numericIdCache[$numericId] = $row; + $this->cache[$row['id']] = $row; + } + } + return $this->numericIdCache[$numericId] ?? null; } public function clearCache() { diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 779e0611591..d80ae041d57 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1099,6 +1099,7 @@ class View { [Filesystem::signal_param_path => $this->getHookPath($path)] ); } + /** @var Storage|null $storage */ [$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix); if ($storage) { return $storage->hash($type, $internalPath, $raw); diff --git a/lib/private/Group/Group.php b/lib/private/Group/Group.php index 9a9996f7f60..2ef4d2ee23f 100644 --- a/lib/private/Group/Group.php +++ b/lib/private/Group/Group.php @@ -354,8 +354,7 @@ class Group implements IGroup { } foreach ($this->backends as $backend) { if ($backend->implementsActions(\OC\Group\Backend::DELETE_GROUP)) { - $result = true; - $backend->deleteGroup($this->gid); + $result = $result || $backend->deleteGroup($this->gid); } } if ($result) { diff --git a/lib/private/Memcache/APCu.php b/lib/private/Memcache/APCu.php index 56345890bf2..f0eb98b9db2 100644 --- a/lib/private/Memcache/APCu.php +++ b/lib/private/Memcache/APCu.php @@ -148,10 +148,7 @@ class APCu extends Cache implements IMemcache { } } - /** - * @return bool - */ - public static function isAvailable() { + public static function isAvailable(): bool { if (!extension_loaded('apcu')) { return false; } elseif (!\OC::$server->get(IniGetWrapper::class)->getBool('apc.enabled')) { diff --git a/lib/private/Memcache/ArrayCache.php b/lib/private/Memcache/ArrayCache.php index b89aff0b7ed..13597a068b3 100644 --- a/lib/private/Memcache/ArrayCache.php +++ b/lib/private/Memcache/ArrayCache.php @@ -153,7 +153,7 @@ class ArrayCache extends Cache implements IMemcache { /** * {@inheritDoc} */ - public static function isAvailable() { + public static function isAvailable(): bool { return true; } } diff --git a/lib/private/Memcache/Factory.php b/lib/private/Memcache/Factory.php index 73206aac011..604f764c03c 100644 --- a/lib/private/Memcache/Factory.php +++ b/lib/private/Memcache/Factory.php @@ -31,6 +31,7 @@ */ namespace OC\Memcache; +use OCP\Profiler\IProfiler; use OCP\ICache; use OCP\ICacheFactory; use OCP\IMemcache; @@ -39,39 +40,39 @@ use Psr\Log\LoggerInterface; class Factory implements ICacheFactory { public const NULL_CACHE = NullCache::class; - /** - * @var string $globalPrefix - */ - private $globalPrefix; + private string $globalPrefix; private LoggerInterface $logger; /** - * @var string $localCacheClass + * @var ?class-string<ICache> $localCacheClass + */ + private ?string $localCacheClass; + + /** + * @var ?class-string<ICache> $distributedCacheClass */ - private $localCacheClass; + private ?string $distributedCacheClass; /** - * @var string $distributedCacheClass + * @var ?class-string<IMemcache> $lockingCacheClass */ - private $distributedCacheClass; + private ?string $lockingCacheClass; + + private string $logFile; + + private IProfiler $profiler; /** - * @var string $lockingCacheClass + * @param string $globalPrefix + * @param LoggerInterface $logger + * @param ?class-string<ICache> $localCacheClass + * @param ?class-string<ICache> $distributedCacheClass + * @param ?class-string<IMemcache> $lockingCacheClass + * @param string $logFile */ - private $lockingCacheClass; - - /** @var string */ - private $logFile; - - public function __construct( - string $globalPrefix, - LoggerInterface $logger, - ?string $localCacheClass = null, - ?string $distributedCacheClass = null, - ?string $lockingCacheClass = null, - string $logFile = '' - ) { + public function __construct(string $globalPrefix, LoggerInterface $logger, IProfiler $profiler, + ?string $localCacheClass = null, ?string $distributedCacheClass = null, ?string $lockingCacheClass = null, string $logFile = '') { $this->logger = $logger; $this->logFile = $logFile; $this->globalPrefix = $globalPrefix; @@ -103,6 +104,7 @@ class Factory implements ICacheFactory { $this->localCacheClass = $localCacheClass; $this->distributedCacheClass = $distributedCacheClass; $this->lockingCacheClass = $lockingCacheClass; + $this->profiler = $profiler; } /** @@ -112,7 +114,19 @@ class Factory implements ICacheFactory { * @return IMemcache */ public function createLocking(string $prefix = ''): IMemcache { - return new $this->lockingCacheClass($this->globalPrefix . '/' . $prefix, $this->logFile); + assert($this->lockingCacheClass !== null); + $cache = new $this->lockingCacheClass($this->globalPrefix . '/' . $prefix); + if ($this->profiler->isEnabled() && $this->lockingCacheClass === '\OC\Memcache\Redis') { + // We only support the profiler with Redis + $cache = new ProfilerWrapperCache($cache, 'Locking'); + $this->profiler->add($cache); + } + + if ($this->lockingCacheClass === Redis::class && + $this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { + $cache = new LoggerWrapperCache($cache, $this->logFile); + } + return $cache; } /** @@ -122,7 +136,19 @@ class Factory implements ICacheFactory { * @return ICache */ public function createDistributed(string $prefix = ''): ICache { - return new $this->distributedCacheClass($this->globalPrefix . '/' . $prefix, $this->logFile); + assert($this->distributedCacheClass !== null); + $cache = new $this->distributedCacheClass($this->globalPrefix . '/' . $prefix); + if ($this->profiler->isEnabled() && $this->distributedCacheClass === '\OC\Memcache\Redis') { + // We only support the profiler with Redis + $cache = new ProfilerWrapperCache($cache, 'Distributed'); + $this->profiler->add($cache); + } + + if ($this->distributedCacheClass === Redis::class && $this->logFile !== '' + && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { + $cache = new LoggerWrapperCache($cache, $this->logFile); + } + return $cache; } /** @@ -132,7 +158,19 @@ class Factory implements ICacheFactory { * @return ICache */ public function createLocal(string $prefix = ''): ICache { - return new $this->localCacheClass($this->globalPrefix . '/' . $prefix, $this->logFile); + assert($this->localCacheClass !== null); + $cache = new $this->localCacheClass($this->globalPrefix . '/' . $prefix); + if ($this->profiler->isEnabled() && $this->localCacheClass === '\OC\Memcache\Redis') { + // We only support the profiler with Redis + $cache = new ProfilerWrapperCache($cache, 'Local'); + $this->profiler->add($cache); + } + + if ($this->localCacheClass === Redis::class && $this->logFile !== '' + && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { + $cache = new LoggerWrapperCache($cache, $this->logFile); + } + return $cache; } /** diff --git a/lib/private/Memcache/LoggerWrapperCache.php b/lib/private/Memcache/LoggerWrapperCache.php new file mode 100644 index 00000000000..55c0e76db79 --- /dev/null +++ b/lib/private/Memcache/LoggerWrapperCache.php @@ -0,0 +1,177 @@ +<?php + +declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Memcache; + +use OCP\IMemcacheTTL; + +/** + * Cache wrapper that logs the cache operation in a log file + */ +class LoggerWrapperCache extends Cache implements IMemcacheTTL { + /** @var Redis */ + protected $wrappedCache; + + /** @var string $logFile */ + private $logFile; + + /** @var string $prefix */ + protected $prefix; + + public function __construct(Redis $wrappedCache, string $logFile) { + parent::__construct($wrappedCache->getPrefix()); + $this->wrappedCache = $wrappedCache; + $this->logFile = $logFile; + } + + /** + * @return string Prefix used for caching purposes + */ + public function getPrefix() { + return $this->prefix; + } + + protected function getNameSpace() { + return $this->prefix; + } + + /** @inheritDoc */ + public function get($key) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::get::' . $key . "\n", + FILE_APPEND + ); + return $this->wrappedCache->get($key); + } + + /** @inheritDoc */ + public function set($key, $value, $ttl = 0) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::set::' . $key . '::' . $ttl . '::' . json_encode($value) . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->set($key, $value, $$ttl); + } + + /** @inheritDoc */ + public function hasKey($key) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::hasKey::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->hasKey($key); + } + + /** @inheritDoc */ + public function remove($key) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::remove::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->remove($key); + } + + /** @inheritDoc */ + public function clear($prefix = '') { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::clear::' . $prefix . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->clear($prefix); + } + + /** @inheritDoc */ + public function add($key, $value, $ttl = 0) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::add::' . $key . '::' . $value . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->add($key, $value, $ttl); + } + + /** @inheritDoc */ + public function inc($key, $step = 1) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::inc::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->inc($key, $step); + } + + /** @inheritDoc */ + public function dec($key, $step = 1) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::dec::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->dec($key, $step); + } + + /** @inheritDoc */ + public function cas($key, $old, $new) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::cas::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->cas($key, $old, $new); + } + + /** @inheritDoc */ + public function cad($key, $old) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::cad::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->cad($key, $old); + } + + /** @inheritDoc */ + public function setTTL($key, $ttl) { + $this->wrappedCache->setTTL($key, $ttl); + } + + public static function isAvailable(): bool { + return true; + } +} diff --git a/lib/private/Memcache/Memcached.php b/lib/private/Memcache/Memcached.php index f78be581d63..db4aa7ba9cc 100644 --- a/lib/private/Memcache/Memcached.php +++ b/lib/private/Memcache/Memcached.php @@ -196,7 +196,7 @@ class Memcached extends Cache implements IMemcache { return $result; } - public static function isAvailable() { + public static function isAvailable(): bool { return extension_loaded('memcached'); } diff --git a/lib/private/Memcache/NullCache.php b/lib/private/Memcache/NullCache.php index 7b56ec932f4..fc41595dfe1 100644 --- a/lib/private/Memcache/NullCache.php +++ b/lib/private/Memcache/NullCache.php @@ -67,7 +67,7 @@ class NullCache extends Cache implements \OCP\IMemcache { return true; } - public static function isAvailable() { + public static function isAvailable(): bool { return true; } } diff --git a/lib/private/Memcache/ProfilerWrapperCache.php b/lib/private/Memcache/ProfilerWrapperCache.php new file mode 100644 index 00000000000..8e9b160ba0e --- /dev/null +++ b/lib/private/Memcache/ProfilerWrapperCache.php @@ -0,0 +1,220 @@ +<?php + +declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Memcache; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\AbstractDataCollector; +use OCP\IMemcacheTTL; + +/** + * Cache wrapper that logs profiling information + */ +class ProfilerWrapperCache extends AbstractDataCollector implements IMemcacheTTL, \ArrayAccess { + /** @var Redis $wrappedCache*/ + protected $wrappedCache; + + /** @var string $prefix */ + protected $prefix; + + /** @var string $type */ + private $type; + + public function __construct(Redis $wrappedCache, string $type) { + $this->prefix = $wrappedCache->getPrefix(); + $this->wrappedCache = $wrappedCache; + $this->type = $type; + $this->data['queries'] = []; + $this->data['cacheHit'] = 0; + $this->data['cacheMiss'] = 0; + } + + public function getPrefix(): string { + return $this->prefix; + } + + /** @inheritDoc */ + public function get($key) { + $start = microtime(true); + $ret = $this->wrappedCache->get($key); + if ($ret === null) { + $this->data['cacheMiss']++; + } else { + $this->data['cacheHit']++; + } + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::get::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function set($key, $value, $ttl = 0) { + $start = microtime(true); + $ret = $this->wrappedCache->set($key, $value, $ttl); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::set::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function hasKey($key) { + $start = microtime(true); + $ret = $this->wrappedCache->hasKey($key); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::hasKey::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function remove($key) { + $start = microtime(true); + $ret = $this->wrappedCache->remove($key); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::remove::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function clear($prefix = '') { + $start = microtime(true); + $ret = $this->wrappedCache->clear($prefix); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::clear::' . $prefix, + ]; + return $ret; + } + + /** @inheritDoc */ + public function add($key, $value, $ttl = 0) { + $start = microtime(true); + $ret = $this->wrappedCache->add($key, $value, $ttl); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::add::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function inc($key, $step = 1) { + $start = microtime(true); + $ret = $this->wrappedCache->inc($key, $step); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::inc::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function dec($key, $step = 1) { + $start = microtime(true); + $ret = $this->wrappedCache->dec($key, $step); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::dev::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function cas($key, $old, $new) { + $start = microtime(true); + $ret = $this->wrappedCache->cas($key, $old, $new); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::cas::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function cad($key, $old) { + $start = microtime(true); + $ret = $this->wrappedCache->cad($key, $old); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::cad::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function setTTL($key, $ttl) { + $this->wrappedCache->setTTL($key, $ttl); + } + + public function offsetExists($offset): bool { + return $this->hasKey($offset); + } + + public function offsetSet($offset, $value): void { + $this->set($offset, $value); + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->get($offset); + } + + public function offsetUnset($offset): void { + $this->remove($offset); + } + + public function collect(Request $request, Response $response, \Throwable $exception = null): void { + // Nothing to do here $data is already ready + } + + public function getName(): string { + return 'cache/' . $this->type . '/' . $this->prefix; + } + + public static function isAvailable(): bool { + return true; + } +} diff --git a/lib/private/Memcache/Redis.php b/lib/private/Memcache/Redis.php index 63180dd8066..9b07da2d99c 100644 --- a/lib/private/Memcache/Redis.php +++ b/lib/private/Memcache/Redis.php @@ -37,38 +37,16 @@ class Redis extends Cache implements IMemcacheTTL { */ private static $cache = null; - private $logFile; - public function __construct($prefix = '', string $logFile = '') { parent::__construct($prefix); - $this->logFile = $logFile; if (is_null(self::$cache)) { self::$cache = \OC::$server->getGetRedisFactory()->getInstance(); } } - /** - * entries in redis get namespaced to prevent collisions between ownCloud instances and users - */ - protected function getNameSpace() { - return $this->prefix; - } - - private function logEnabled(): bool { - return $this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile)); - } - public function get($key) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::get::' . $key . "\n", - FILE_APPEND - ); - } - - $result = self::$cache->get($this->getNameSpace() . $key); - if ($result === false && !self::$cache->exists($this->getNameSpace() . $key)) { + $result = self::$cache->get($this->getPrefix() . $key); + if ($result === false && !self::$cache->exists($this->getPrefix() . $key)) { return null; } else { return json_decode($result, true); @@ -76,43 +54,19 @@ class Redis extends Cache implements IMemcacheTTL { } public function set($key, $value, $ttl = 0) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::set::' . $key . '::' . $ttl . '::' . json_encode($value) . "\n", - FILE_APPEND - ); - } - if ($ttl > 0) { - return self::$cache->setex($this->getNameSpace() . $key, $ttl, json_encode($value)); + return self::$cache->setex($this->getPrefix() . $key, $ttl, json_encode($value)); } else { - return self::$cache->set($this->getNameSpace() . $key, json_encode($value)); + return self::$cache->set($this->getPrefix() . $key, json_encode($value)); } } public function hasKey($key) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::hasKey::' . $key . "\n", - FILE_APPEND - ); - } - - return (bool)self::$cache->exists($this->getNameSpace() . $key); + return (bool)self::$cache->exists($this->getPrefix() . $key); } public function remove($key) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::remove::' . $key . "\n", - FILE_APPEND - ); - } - - if (self::$cache->del($this->getNameSpace() . $key)) { + if (self::$cache->del($this->getPrefix() . $key)) { return true; } else { return false; @@ -120,15 +74,7 @@ class Redis extends Cache implements IMemcacheTTL { } public function clear($prefix = '') { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::clear::' . $prefix . "\n", - FILE_APPEND - ); - } - - $prefix = $this->getNameSpace() . $prefix . '*'; + $prefix = $this->getPrefix() . $prefix . '*'; $keys = self::$cache->keys($prefix); $deleted = self::$cache->del($keys); @@ -153,14 +99,6 @@ class Redis extends Cache implements IMemcacheTTL { if ($ttl !== 0 && is_int($ttl)) { $args['ex'] = $ttl; } - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::add::' . $key . '::' . $value . "\n", - FILE_APPEND - ); - } - return self::$cache->set($this->getPrefix() . $key, $value, $args); } @@ -173,15 +111,7 @@ class Redis extends Cache implements IMemcacheTTL { * @return int | bool */ public function inc($key, $step = 1) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::inc::' . $key . "\n", - FILE_APPEND - ); - } - - return self::$cache->incrBy($this->getNameSpace() . $key, $step); + return self::$cache->incrBy($this->getPrefix() . $key, $step); } /** @@ -192,18 +122,10 @@ class Redis extends Cache implements IMemcacheTTL { * @return int | bool */ public function dec($key, $step = 1) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::dec::' . $key . "\n", - FILE_APPEND - ); - } - if (!$this->hasKey($key)) { return false; } - return self::$cache->decrBy($this->getNameSpace() . $key, $step); + return self::$cache->decrBy($this->getPrefix() . $key, $step); } /** @@ -215,21 +137,13 @@ class Redis extends Cache implements IMemcacheTTL { * @return bool */ public function cas($key, $old, $new) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::cas::' . $key . "\n", - FILE_APPEND - ); - } - if (!is_int($new)) { $new = json_encode($new); } - self::$cache->watch($this->getNameSpace() . $key); + self::$cache->watch($this->getPrefix() . $key); if ($this->get($key) === $old) { $result = self::$cache->multi() - ->set($this->getNameSpace() . $key, $new) + ->set($this->getPrefix() . $key, $new) ->exec(); return $result !== false; } @@ -245,18 +159,10 @@ class Redis extends Cache implements IMemcacheTTL { * @return bool */ public function cad($key, $old) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::cad::' . $key . "\n", - FILE_APPEND - ); - } - - self::$cache->watch($this->getNameSpace() . $key); + self::$cache->watch($this->getPrefix() . $key); if ($this->get($key) === $old) { $result = self::$cache->multi() - ->del($this->getNameSpace() . $key) + ->del($this->getPrefix() . $key) ->exec(); return $result !== false; } @@ -265,10 +171,10 @@ class Redis extends Cache implements IMemcacheTTL { } public function setTTL($key, $ttl) { - self::$cache->expire($this->getNameSpace() . $key, $ttl); + self::$cache->expire($this->getPrefix() . $key, $ttl); } - public static function isAvailable() { + public static function isAvailable(): bool { return \OC::$server->getGetRedisFactory()->isAvailable(); } } diff --git a/lib/private/Profiler/FileProfilerStorage.php b/lib/private/Profiler/FileProfilerStorage.php new file mode 100644 index 00000000000..ce09ed51ed9 --- /dev/null +++ b/lib/private/Profiler/FileProfilerStorage.php @@ -0,0 +1,286 @@ +<?php + +declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * @author Alexandre Salomé <alexandre.salome@gmail.com> + * + * @license AGPL-3.0-or-later AND MIT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Profiler; + +use OCP\Profiler\IProfile; + +/** + * Storage for profiler using files. + */ +class FileProfilerStorage { + // Folder where profiler data are stored. + private string $folder; + + /** + * Constructs the file storage using a "dsn-like" path. + * + * Example : "file:/path/to/the/storage/folder" + * + * @throws \RuntimeException + */ + public function __construct(string $folder) { + $this->folder = $folder; + + if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) { + throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder)); + } + } + + public function find(?string $url, ?int $limit, ?string $method, int $start = null, int $end = null, string $statusCode = null): array { + $file = $this->getIndexFilename(); + + if (!file_exists($file)) { + return []; + } + + $file = fopen($file, 'r'); + fseek($file, 0, \SEEK_END); + + $result = []; + while (\count($result) < $limit && $line = $this->readLineFromFile($file)) { + $values = str_getcsv($line); + [$csvToken, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode] = $values; + $csvTime = (int) $csvTime; + + if ($url && false === strpos($csvUrl, $url) || $method && false === strpos($csvMethod, $method) || $statusCode && false === strpos($csvStatusCode, $statusCode)) { + continue; + } + + if (!empty($start) && $csvTime < $start) { + continue; + } + + if (!empty($end) && $csvTime > $end) { + continue; + } + + $result[$csvToken] = [ + 'token' => $csvToken, + 'method' => $csvMethod, + 'url' => $csvUrl, + 'time' => $csvTime, + 'parent' => $csvParent, + 'status_code' => $csvStatusCode, + ]; + } + + fclose($file); + + return array_values($result); + } + + public function purge(): void { + $flags = \FilesystemIterator::SKIP_DOTS; + $iterator = new \RecursiveDirectoryIterator($this->folder, $flags); + $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($iterator as $file) { + if (is_file($file)) { + unlink($file); + } else { + rmdir($file); + } + } + } + + public function read(string $token): ?IProfile { + if (!$token || !file_exists($file = $this->getFilename($token))) { + return null; + } + + if (\function_exists('gzcompress')) { + $file = 'compress.zlib://'.$file; + } + + return $this->createProfileFromData($token, unserialize(file_get_contents($file))); + } + + /** + * @throws \RuntimeException + */ + public function write(IProfile $profile): bool { + $file = $this->getFilename($profile->getToken()); + + $profileIndexed = is_file($file); + if (!$profileIndexed) { + // Create directory + $dir = \dirname($file); + if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir)); + } + } + + $profileToken = $profile->getToken(); + // when there are errors in sub-requests, the parent and/or children tokens + // may equal the profile token, resulting in infinite loops + $parentToken = $profile->getParentToken() !== $profileToken ? $profile->getParentToken() : null; + $childrenToken = array_filter(array_map(function (IProfile $p) use ($profileToken) { + return $profileToken !== $p->getToken() ? $p->getToken() : null; + }, $profile->getChildren())); + + // Store profile + $data = [ + 'token' => $profileToken, + 'parent' => $parentToken, + 'children' => $childrenToken, + 'data' => $profile->getCollectors(), + 'method' => $profile->getMethod(), + 'url' => $profile->getUrl(), + 'time' => $profile->getTime(), + 'status_code' => $profile->getStatusCode(), + ]; + + $context = stream_context_create(); + + if (\function_exists('gzcompress')) { + $file = 'compress.zlib://'.$file; + stream_context_set_option($context, 'zlib', 'level', 3); + } + + if (false === file_put_contents($file, serialize($data), 0, $context)) { + return false; + } + + if (!$profileIndexed) { + // Add to index + if (false === $file = fopen($this->getIndexFilename(), 'a')) { + return false; + } + + fputcsv($file, [ + $profile->getToken(), + $profile->getMethod(), + $profile->getUrl(), + $profile->getTime(), + $profile->getParentToken(), + $profile->getStatusCode(), + ]); + fclose($file); + } + + return true; + } + + /** + * Gets filename to store data, associated to the token. + * + * @return string The profile filename + */ + protected function getFilename(string $token): string { + // Uses 4 last characters, because first are mostly the same. + $folderA = substr($token, -2, 2); + $folderB = substr($token, -4, 2); + + return $this->folder.'/'.$folderA.'/'.$folderB.'/'.$token; + } + + /** + * Gets the index filename. + * + * @return string The index filename + */ + protected function getIndexFilename(): string { + return $this->folder.'/index.csv'; + } + + /** + * Reads a line in the file, backward. + * + * This function automatically skips the empty lines and do not include the line return in result value. + * + * @param resource $file The file resource, with the pointer placed at the end of the line to read + * + * @return ?string A string representing the line or null if beginning of file is reached + */ + protected function readLineFromFile($file): ?string { + $line = ''; + $position = ftell($file); + + if (0 === $position) { + return null; + } + + while (true) { + $chunkSize = min($position, 1024); + $position -= $chunkSize; + fseek($file, $position); + + if (0 === $chunkSize) { + // bof reached + break; + } + + $buffer = fread($file, $chunkSize); + + if (false === ($upTo = strrpos($buffer, "\n"))) { + $line = $buffer.$line; + continue; + } + + $position += $upTo; + $line = substr($buffer, $upTo + 1).$line; + fseek($file, max(0, $position), \SEEK_SET); + + if ('' !== $line) { + break; + } + } + + return '' === $line ? null : $line; + } + + protected function createProfileFromData(string $token, array $data, IProfile $parent = null): IProfile { + $profile = new Profile($token); + $profile->setMethod($data['method']); + $profile->setUrl($data['url']); + $profile->setTime($data['time']); + $profile->setStatusCode($data['status_code']); + $profile->setCollectors($data['data']); + + if (!$parent && $data['parent']) { + $parent = $this->read($data['parent']); + } + + if ($parent) { + $profile->setParent($parent); + } + + foreach ($data['children'] as $token) { + if (!$token || !file_exists($file = $this->getFilename($token))) { + continue; + } + + if (\function_exists('gzcompress')) { + $file = 'compress.zlib://'.$file; + } + + $profile->addChild($this->createProfileFromData($token, unserialize(file_get_contents($file)), $profile)); + } + + return $profile; + } +} diff --git a/lib/private/Profiler/Profile.php b/lib/private/Profiler/Profile.php new file mode 100644 index 00000000000..648c49c0330 --- /dev/null +++ b/lib/private/Profiler/Profile.php @@ -0,0 +1,168 @@ +<?php + +declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Profiler; + +use OCP\DataCollector\IDataCollector; +use OCP\Profiler\IProfile; + +class Profile implements \JsonSerializable, IProfile { + private string $token; + + private ?int $time = null; + + private ?string $url = null; + + private ?string $method = null; + + private ?int $statusCode = null; + + /** @var array<string, IDataCollector> */ + private array $collectors = []; + + private ?IProfile $parent = null; + + /** @var IProfile[] */ + private array $children = []; + + public function __construct(string $token) { + $this->token = $token; + } + + public function getToken(): string { + return $this->token; + } + + public function setToken(string $token): void { + $this->token = $token; + } + + public function getTime(): ?int { + return $this->time; + } + + public function setTime(int $time): void { + $this->time = $time; + } + + public function getUrl(): ?string { + return $this->url; + } + + public function setUrl(string $url): void { + $this->url = $url; + } + + public function getMethod(): ?string { + return $this->method; + } + + public function setMethod(string $method): void { + $this->method = $method; + } + + public function getStatusCode(): ?int { + return $this->statusCode; + } + + public function setStatusCode(int $statusCode): void { + $this->statusCode = $statusCode; + } + + public function addCollector(IDataCollector $collector) { + $this->collectors[$collector->getName()] = $collector; + } + + public function getParent(): ?IProfile { + return $this->parent; + } + + public function setParent(?IProfile $parent): void { + $this->parent = $parent; + } + + public function getParentToken(): ?string { + return $this->parent ? $this->parent->getToken() : null; + } + + /** @return IProfile[] */ + public function getChildren(): array { + return $this->children; + } + + /** + * @param IProfile[] $children + */ + public function setChildren(array $children): void { + $this->children = []; + foreach ($children as $child) { + $this->addChild($child); + } + } + + public function addChild(IProfile $profile): void { + $this->children[] = $profile; + $profile->setParent($this); + } + + /** + * @return IDataCollector[] + */ + public function getCollectors(): array { + return $this->collectors; + } + + /** + * @param IDataCollector[] $collectors + */ + public function setCollectors(array $collectors): void { + $this->collectors = $collectors; + } + + public function __sleep(): array { + return ['token', 'parent', 'children', 'collectors', 'method', 'url', 'time', 'statusCode']; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() { + // Everything but parent + return [ + 'token' => $this->token, + 'method' => $this->method, + 'children' => $this->children, + 'url' => $this->url, + 'statusCode' => $this->statusCode, + 'time' => $this->time, + 'collectors' => $this->collectors, + ]; + } + + public function getCollector(string $collectorName): ?IDataCollector { + if (!array_key_exists($collectorName, $this->collectors)) { + return null; + } + return $this->collectors[$collectorName]; + } +} diff --git a/lib/private/Profiler/Profiler.php b/lib/private/Profiler/Profiler.php new file mode 100644 index 00000000000..8aa800fbc6d --- /dev/null +++ b/lib/private/Profiler/Profiler.php @@ -0,0 +1,105 @@ +<?php + +declare(strict_types = 1); + +/** + * @copyright 2021 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Profiler; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\IDataCollector; +use OCP\Profiler\IProfiler; +use OCP\Profiler\IProfile; +use OC\SystemConfig; + +class Profiler implements IProfiler { + /** @var array<string, IDataCollector> */ + private array $dataCollectors = []; + + private ?FileProfilerStorage $storage = null; + + private bool $enabled = false; + + public function __construct(SystemConfig $config) { + $this->enabled = $config->getValue('profiler', false); + if ($this->enabled) { + $this->storage = new FileProfilerStorage($config->getValue('datadirectory', \OC::$SERVERROOT . '/data') . '/profiler'); + } + } + + public function add(IDataCollector $dataCollector): void { + $this->dataCollectors[$dataCollector->getName()] = $dataCollector; + } + + public function loadProfileFromResponse(Response $response): ?IProfile { + if (!$token = $response->getHeaders()['X-Debug-Token']) { + return null; + } + + return $this->loadProfile($token); + } + + public function loadProfile(string $token): ?IProfile { + return $this->storage->read($token); + } + + public function saveProfile(IProfile $profile): bool { + return $this->storage->write($profile); + } + + public function collect(Request $request, Response $response): IProfile { + $profile = new Profile($request->getId()); + $profile->setTime(time()); + $profile->setUrl($request->getRequestUri()); + $profile->setMethod($request->getMethod()); + $profile->setStatusCode($response->getStatus()); + foreach ($this->dataCollectors as $dataCollector) { + $dataCollector->collect($request, $response, null); + + // We clone for subrequests + $profile->addCollector(clone $dataCollector); + } + return $profile; + } + + /** + * @return array[] + */ + public function find(?string $url, ?int $limit, ?string $method, ?int $start, ?int $end, + string $statusCode = null): array { + return $this->storage->find($url, $limit, $method, $start, $end, $statusCode); + } + + public function dataProviders(): array { + return array_keys($this->dataCollectors); + } + + public function isEnabled(): bool { + return $this->enabled; + } + + public function setEnabled(bool $enabled): void { + $this->enabled = $enabled; + } +} diff --git a/lib/private/Profiler/RoutingDataCollector.php b/lib/private/Profiler/RoutingDataCollector.php new file mode 100644 index 00000000000..e6659230879 --- /dev/null +++ b/lib/private/Profiler/RoutingDataCollector.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Profiler; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\AbstractDataCollector; + +class RoutingDataCollector extends AbstractDataCollector { + private string $appName; + private string $controllerName; + private string $actionName; + + public function __construct(string $appName, string $controllerName, string $actionName) { + $this->appName = $appName; + $this->controllerName = $controllerName; + $this->actionName = $actionName; + } + + public function collect(Request $request, Response $response, \Throwable $exception = null): void { + $this->data = [ + 'appName' => $this->appName, + 'controllerName' => $this->controllerName, + 'actionName' => $this->actionName, + ]; + } + + public function getName(): string { + return 'router'; + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 682e6fa06ce..b214ba3ce54 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -260,6 +260,8 @@ use OCA\Files_External\Service\UserStoragesService; use OCA\Files_External\Service\UserGlobalStoragesService; use OCA\Files_External\Service\GlobalStoragesService; use OCA\Files_External\Service\BackendService; +use OCP\Profiler\IProfiler; +use OC\Profiler\Profiler; /** * Class Server @@ -344,6 +346,10 @@ class Server extends ServerContainer implements IServerContainer { ); }); + $this->registerService(IProfiler::class, function (Server $c) { + return new Profiler($c->get(SystemConfig::class)); + }); + $this->registerService(\OCP\Encryption\IManager::class, function (Server $c) { $view = new View(); $util = new Encryption\Util( @@ -691,9 +697,9 @@ class Server extends ServerContainer implements IServerContainer { $this->registerDeprecatedAlias('UserCache', ICache::class); $this->registerService(Factory::class, function (Server $c) { - $arrayCacheFactory = new \OC\Memcache\Factory( - '', - $c->get(LoggerInterface::class), + $profiler = $c->get(IProfiler::class); + $arrayCacheFactory = new \OC\Memcache\Factory('', $c->get(LoggerInterface::class), + $profiler, ArrayCache::class, ArrayCache::class, ArrayCache::class @@ -717,9 +723,9 @@ class Server extends ServerContainer implements IServerContainer { $instanceId = \OC_Util::getInstanceId(); $path = \OC::$SERVERROOT; $prefix = md5($instanceId . '-' . $version . '-' . $path); - return new \OC\Memcache\Factory( - $prefix, + return new \OC\Memcache\Factory($prefix, $c->get(LoggerInterface::class), + $profiler, $config->getSystemValue('memcache.local', null), $config->getSystemValue('memcache.distributed', null), $config->getSystemValue('memcache.locking', null), @@ -769,6 +775,7 @@ class Server extends ServerContainer implements IServerContainer { $c->get(KnownUserService::class) ); }); + $this->registerAlias(IAvatarManager::class, AvatarManager::class); /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('AvatarManager', AvatarManager::class); @@ -861,7 +868,6 @@ class Server extends ServerContainer implements IServerContainer { } $connectionParams = $factory->createConnectionParams(); $connection = $factory->getConnection($type, $connectionParams); - $connection->getConfiguration()->setSQLLogger($c->getQueryLogger()); return $connection; }); /** @deprecated 19.0.0 */ diff --git a/lib/private/legacy/OC_Helper.php b/lib/private/legacy/OC_Helper.php index efb9252e346..547ffef8607 100644 --- a/lib/private/legacy/OC_Helper.php +++ b/lib/private/legacy/OC_Helper.php @@ -485,7 +485,7 @@ class OC_Helper { * @return array * @throws \OCP\Files\NotFoundException */ - public static function getStorageInfo($path, $rootInfo = null) { + public static function getStorageInfo($path, $rootInfo = null, $includeMountPoints = true) { // return storage info without adding mount points $includeExtStorage = \OC::$server->getSystemConfig()->getValue('quota_include_external_storage', false); @@ -495,7 +495,7 @@ class OC_Helper { if (!$rootInfo instanceof \OCP\Files\FileInfo) { throw new \OCP\Files\NotFoundException(); } - $used = $rootInfo->getSize(); + $used = $rootInfo->getSize($includeMountPoints); if ($used < 0) { $used = 0; } diff --git a/lib/public/DataCollector/AbstractDataCollector.php b/lib/public/DataCollector/AbstractDataCollector.php new file mode 100644 index 00000000000..68298671b7b --- /dev/null +++ b/lib/public/DataCollector/AbstractDataCollector.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * @author Fabien Potencier <fabien@symfony.com> + * + * @license AGPL-3.0-or-later AND MIT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\DataCollector; + +/** + * Children of this class must store the collected data in + * the data property. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Bernhard Schussek <bschussek@symfony.com> + * @author Carl Schwan <carl@carlschwan.eu> + * @since 24.0.0 + */ +abstract class AbstractDataCollector implements IDataCollector, \JsonSerializable { + /** @var array */ + protected $data = []; + + /** + * @since 24.0.0 + */ + public function getName(): string { + return static::class; + } + + /** + * Reset the state of the profiler. By default it only empties the + * $this->data contents, but you can override this method to do + * additional cleaning. + * @since 24.0.0 + */ + public function reset(): void { + $this->data = []; + } + + /** + * @since 24.0.0 + */ + public function __sleep(): array { + return ['data']; + } + + /** + * @internal to prevent implementing \Serializable + * @since 24.0.0 + */ + final protected function serialize() { + } + + /** + * @internal to prevent implementing \Serializable + * @since 24.0.0 + */ + final protected function unserialize(string $data) { + } + + /** + * @since 24.0.0 + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() { + return $this->data; + } +} diff --git a/lib/public/DataCollector/IDataCollector.php b/lib/public/DataCollector/IDataCollector.php new file mode 100644 index 00000000000..0fb914727df --- /dev/null +++ b/lib/public/DataCollector/IDataCollector.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * @author Fabien Potencier <fabien@symfony.com> + * + * @license AGPL-3.0-or-later AND MIT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\DataCollector; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; + +/** + * DataCollectorInterface. + * + * @since 24.0.0 + */ +interface IDataCollector { + /** + * Collects data for the given Request and Response. + * @since 24.0.0 + */ + public function collect(Request $request, Response $response, \Throwable $exception = null): void; + + /** + * Reset the state of the profiler. + * @since 24.0.0 + */ + public function reset(): void; + + /** + * Returns the name of the collector. + * @since 24.0.0 + */ + public function getName(): string; +} diff --git a/lib/public/ICache.php b/lib/public/ICache.php index 47b0e2f4c3d..0e818277f60 100644 --- a/lib/public/ICache.php +++ b/lib/public/ICache.php @@ -76,4 +76,10 @@ interface ICache { * @since 6.0.0 */ public function clear($prefix = ''); + + /** + * Check if the cache implementation is available + * @since 24.0.0 + */ + public static function isAvailable(): bool; } diff --git a/lib/public/Profiler/IProfile.php b/lib/public/Profiler/IProfile.php new file mode 100644 index 00000000000..1831496a5a7 --- /dev/null +++ b/lib/public/Profiler/IProfile.php @@ -0,0 +1,168 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Profiler; + +use OCP\DataCollector\IDataCollector; + +/** + * This interface store the results of the profiling of one + * request. You can get the saved profiles from the @see IProfiler. + * + * ```php + * <?php + * $profiler = \OC::$server->get(IProfiler::class); + * $profiles = $profiler->find('/settings/users', 10); + * ``` + * + * This interface is meant to be used directly and not extended. + * @since 24.0.0 + */ +interface IProfile { + /** + * Get the token of the profile + * @since 24.0.0 + */ + public function getToken(): string; + + /** + * Set the token of the profile + * @since 24.0.0 + */ + public function setToken(string $token): void; + + /** + * Get the time of the profile + * @since 24.0.0 + */ + public function getTime(): ?int; + + /** + * Set the time of the profile + * @since 24.0.0 + */ + public function setTime(int $time): void; + + /** + * Get the url of the profile + * @since 24.0.0 + */ + public function getUrl(): ?string; + + /** + * Set the url of the profile + * @since 24.0.0 + */ + public function setUrl(string $url): void; + + /** + * Get the method of the profile + * @since 24.0.0 + */ + public function getMethod(): ?string; + + /** + * Set the method of the profile + * @since 24.0.0 + */ + public function setMethod(string $method): void; + + /** + * Get the status code of the profile + * @since 24.0.0 + */ + public function getStatusCode(): ?int; + + /** + * Set the status code of the profile + * @since 24.0.0 + */ + public function setStatusCode(int $statusCode): void; + + /** + * Add a data collector to the profile + * @since 24.0.0 + */ + public function addCollector(IDataCollector $collector); + + /** + * Get the parent profile to this profile + * @since 24.0.0 + */ + public function getParent(): ?IProfile; + + /** + * Set the parent profile to this profile + * @since 24.0.0 + */ + public function setParent(?IProfile $parent): void; + + /** + * Get the parent token to this profile + * @since 24.0.0 + */ + public function getParentToken(): ?string; + + /** + * Get the profile's children + * @return IProfile[] + * @since 24.0.0 + **/ + public function getChildren(): array; + + /** + * Set the profile's children + * @param IProfile[] $children + * @since 24.0.0 + */ + public function setChildren(array $children): void; + + /** + * Add the child profile + * @since 24.0.0 + */ + public function addChild(IProfile $profile): void; + + /** + * Get all the data collectors + * @return IDataCollector[] + * @since 24.0.0 + */ + public function getCollectors(): array; + + /** + * Set all the data collectors + * @param IDataCollector[] $collectors + * @since 24.0.0 + */ + public function setCollectors(array $collectors): void; + + /** + * Get a data collector by name + * @since 24.0.0 + */ + public function getCollector(string $collectorName): ?IDataCollector; +} diff --git a/lib/public/Profiler/IProfiler.php b/lib/public/Profiler/IProfiler.php new file mode 100644 index 00000000000..78325089523 --- /dev/null +++ b/lib/public/Profiler/IProfiler.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Profiler; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\IDataCollector; + +/** + * This interface allows to interact with the built-in Nextcloud profiler. + * @since 24.0.0 + */ +interface IProfiler { + /** + * Add a new data collector to the profiler. This allows to later on + * collect all the data from every registered collector. + * + * @see IDataCollector + * @since 24.0.0 + */ + public function add(IDataCollector $dataCollector): void; + + /** + * Load a profile from a response object + * @since 24.0.0 + */ + public function loadProfileFromResponse(Response $response): ?IProfile; + + /** + * Load a profile from the response token + * @since 24.0.0 + */ + public function loadProfile(string $token): ?IProfile; + + /** + * Save a profile on the disk. This allows to later load it again in the + * profiler user interface. + * @since 24.0.0 + */ + public function saveProfile(IProfile $profile): bool; + + /** + * Find a profile from various search parameters + * @since 24.0.0 + */ + public function find(?string $url, ?int $limit, ?string $method, ?int $start, ?int $end, string $statusCode = null): array; + + /** + * Get the list of data providers by identifier + * @return string[] + * @since 24.0.0 + */ + public function dataProviders(): array; + + /** + * Check if the profiler is enabled. + * + * If it is not enabled, data provider shouldn't be created and + * shouldn't collect any data. + * @since 24.0.0 + */ + public function isEnabled(): bool; + + /** + * Set if the profiler is enabled. + * @see isEnabled + * @since 24.0.0 + */ + public function setEnabled(bool $enabled): void; + + /** + * Collect all the information from the current request and construct + * a IProfile from it. + * @since 24.0.0 + */ + public function collect(Request $request, Response $response): IProfile; +} diff --git a/tests/lib/AppFramework/AppTest.php b/tests/lib/AppFramework/AppTest.php index 595a556a9a8..3dc62246e97 100644 --- a/tests/lib/AppFramework/AppTest.php +++ b/tests/lib/AppFramework/AppTest.php @@ -71,7 +71,7 @@ class AppTest extends \Test\TestCase { $this->container[$this->controllerName] = $this->controller; $this->container['Dispatcher'] = $this->dispatcher; $this->container['OCP\\AppFramework\\Http\\IOutput'] = $this->io; - $this->container['urlParams'] = []; + $this->container['urlParams'] = ['_route' => 'not-profiler']; $this->appPath = __DIR__ . '/../../../apps/namespacetestapp'; $infoXmlPath = $this->appPath . '/appinfo/info.xml'; @@ -183,6 +183,7 @@ class AppTest extends \Test\TestCase { public function testCoreApp() { $this->container['AppName'] = 'core'; $this->container['OC\Core\Controller\Foo'] = $this->controller; + $this->container['urlParams'] = ['_route' => 'not-profiler']; $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; $this->dispatcher->expects($this->once()) @@ -200,6 +201,7 @@ class AppTest extends \Test\TestCase { public function testSettingsApp() { $this->container['AppName'] = 'settings'; $this->container['OCA\Settings\Controller\Foo'] = $this->controller; + $this->container['urlParams'] = ['_route' => 'not-profiler']; $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; $this->dispatcher->expects($this->once()) @@ -217,6 +219,7 @@ class AppTest extends \Test\TestCase { public function testApp() { $this->container['AppName'] = 'bar'; $this->container['OCA\Bar\Controller\Foo'] = $this->controller; + $this->container['urlParams'] = ['_route' => 'not-profiler']; $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; $this->dispatcher->expects($this->once()) diff --git a/tests/lib/Memcache/FactoryTest.php b/tests/lib/Memcache/FactoryTest.php index 0e995865b5d..f16f70eddc2 100644 --- a/tests/lib/Memcache/FactoryTest.php +++ b/tests/lib/Memcache/FactoryTest.php @@ -23,12 +23,13 @@ namespace Test\Memcache; use OC\Memcache\NullCache; use Psr\Log\LoggerInterface; +use OCP\Profiler\IProfiler; class Test_Factory_Available_Cache1 extends NullCache { public function __construct($prefix = '') { } - public static function isAvailable() { + public static function isAvailable(): bool { return true; } } @@ -37,7 +38,7 @@ class Test_Factory_Available_Cache2 extends NullCache { public function __construct($prefix = '') { } - public static function isAvailable() { + public static function isAvailable(): bool { return true; } } @@ -46,7 +47,7 @@ class Test_Factory_Unavailable_Cache1 extends NullCache { public function __construct($prefix = '') { } - public static function isAvailable() { + public static function isAvailable(): bool { return false; } } @@ -55,7 +56,7 @@ class Test_Factory_Unavailable_Cache2 extends NullCache { public function __construct($prefix = '') { } - public static function isAvailable() { + public static function isAvailable(): bool { return false; } } @@ -119,7 +120,8 @@ class FactoryTest extends \Test\TestCase { public function testCacheAvailability($localCache, $distributedCache, $lockingCache, $expectedLocalCache, $expectedDistributedCache, $expectedLockingCache) { $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); - $factory = new \OC\Memcache\Factory('abc', $logger, $localCache, $distributedCache, $lockingCache); + $profiler = $this->getMockBuilder(IProfiler::class)->getMock(); + $factory = new \OC\Memcache\Factory('abc', $logger, $profiler, $localCache, $distributedCache, $lockingCache); $this->assertTrue(is_a($factory->createLocal(), $expectedLocalCache)); $this->assertTrue(is_a($factory->createDistributed(), $expectedDistributedCache)); $this->assertTrue(is_a($factory->createLocking(), $expectedLockingCache)); @@ -132,6 +134,7 @@ class FactoryTest extends \Test\TestCase { $this->expectException(\OCP\HintException::class); $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); - new \OC\Memcache\Factory('abc', $logger, $localCache, $distributedCache); + $profiler = $this->getMockBuilder(IProfiler::class)->getMock(); + new \OC\Memcache\Factory('abc', $logger, $profiler, $localCache, $distributedCache); } } |