diff options
Diffstat (limited to 'apps')
40 files changed, 329 insertions, 174 deletions
diff --git a/apps/dav/l10n/fr.js b/apps/dav/l10n/fr.js index 95dd7ff3414..92a95ba9995 100644 --- a/apps/dav/l10n/fr.js +++ b/apps/dav/l10n/fr.js @@ -37,7 +37,7 @@ OC.L10N.register( "{actor} restored event {event} of calendar {calendar}" : "{actor} a restauré l'événement {event} dans l'agenda {calendar}", "You restored event {event} of calendar {calendar}" : "Vous avez restauré l'événement {event} dans l'agenda {calendar}", "Busy" : "Occupé", - "{actor} created to-do {todo} in list {calendar}" : "{actor} a créé le pense-bête {todo} dans la liste {calendar}", + "{actor} created to-do {todo} in list {calendar}" : "{actor} a créé la tâche {todo} dans la liste {calendar}", "You created to-do {todo} in list {calendar}" : "Vous avez créé un pense-bête {todo} dans la liste {calendar}", "{actor} deleted to-do {todo} from list {calendar}" : "{actor} a supprimé un pense-bête {todo} de la liste {calendar}", "You deleted to-do {todo} from list {calendar}" : "Vous avez supprimé le pense-bête {todo} de la liste {calendar}", diff --git a/apps/dav/l10n/fr.json b/apps/dav/l10n/fr.json index 60901d2d3ff..b266d7c3dff 100644 --- a/apps/dav/l10n/fr.json +++ b/apps/dav/l10n/fr.json @@ -35,7 +35,7 @@ "{actor} restored event {event} of calendar {calendar}" : "{actor} a restauré l'événement {event} dans l'agenda {calendar}", "You restored event {event} of calendar {calendar}" : "Vous avez restauré l'événement {event} dans l'agenda {calendar}", "Busy" : "Occupé", - "{actor} created to-do {todo} in list {calendar}" : "{actor} a créé le pense-bête {todo} dans la liste {calendar}", + "{actor} created to-do {todo} in list {calendar}" : "{actor} a créé la tâche {todo} dans la liste {calendar}", "You created to-do {todo} in list {calendar}" : "Vous avez créé un pense-bête {todo} dans la liste {calendar}", "{actor} deleted to-do {todo} from list {calendar}" : "{actor} a supprimé un pense-bête {todo} de la liste {calendar}", "You deleted to-do {todo} from list {calendar}" : "Vous avez supprimé le pense-bête {todo} de la liste {calendar}", diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index a6c9b8b4ebe..6b6f622a5a7 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -436,7 +436,7 @@ class FilesPlugin extends ServerPlugin { \OC::$server->get(LoggerInterface::class)->debug('Inefficient fetching of metadata'); } - return json_encode((object)$sizeMetadata->getMetadata(), JSON_THROW_ON_ERROR); + return $sizeMetadata->getValue(); }); } } diff --git a/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php index a74cb139966..c6365cf3168 100644 --- a/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php @@ -63,7 +63,7 @@ class TestViewDirectory extends \OC\Files\View { return $this->canRename; } - public function getRelativePath($path) { + public function getRelativePath($path): ?string { return $path; } } @@ -73,7 +73,6 @@ class TestViewDirectory extends \OC\Files\View { * @group DB */ class DirectoryTest extends \Test\TestCase { - use UserTrait; /** @var \OC\Files\View | \PHPUnit\Framework\MockObject\MockObject */ diff --git a/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php index d219888ef15..8d6bfc1764b 100644 --- a/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php @@ -62,8 +62,7 @@ class ObjectTreeTest extends \Test\TestCase { $view = $this->createMock(View::class); $view->expects($this->once()) ->method('verifyPath') - ->with($targetParent) - ->willReturn(true); + ->with($targetParent); $view->expects($this->once()) ->method('file_exists') ->with($targetPath) diff --git a/apps/encryption/l10n/es.js b/apps/encryption/l10n/es.js index 7edb0917fc7..83b7ef0e83e 100644 --- a/apps/encryption/l10n/es.js +++ b/apps/encryption/l10n/es.js @@ -32,6 +32,7 @@ OC.L10N.register( "Cannot read this file, probably this is a shared file. Please ask the file owner to reshare the file with you." : "No se puede leer este archivo, probablemente se trate de un archivo compartido. Por favor, pida al propietario del archivo que vuelva a compartirlo con usted.", "Default encryption module" : "Módulo de cifrado por defecto", "Default encryption module for server-side encryption" : "Módulo de cifrado por defecto para el cifrado en el lado del servidor", + "In order to use this encryption module you need to enable server-side encryption in the admin settings. Once enabled this module will encrypt all your files transparently. The encryption is based on AES 256 keys.\nThe module will not touch existing files, only new files will be encrypted after server-side encryption was enabled. It is also not possible to disable the encryption again and switch back to an unencrypted system.\nPlease read the documentation to know all implications before you decide to enable server-side encryption." : "De manera de usar este módulo de cifrado necesitas activar el cifrado del lado del servidor en la configuraciones de administrador. Una vez que esté habilitado este módulo cifrará todos tus archivos de manera transparente. El cifrado está basado en llaves AES-256\nEl módulo no tocará los archivos existentes, solo los archivos nuevos serán cifrados una vez que el cifrado del lado del servidor se habilite. Además, no es posible deshabilitar el cifrado de nuevo y cambiar a un sistema sin cifrado.\nPor favor lea la documentación para que entienda todas las implicaciones antes de que decida habilitar el cifrado del lado del servidor.", "Hey there,\n\nThe administration 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" : "Hola,\n\nEl administrador habilitó el cifrado de datos del servidor. Tus archivos fueron cifrados con la contraseña \"%s\".\n\nPor favor, inicia sesión en la interfaz web, ve a la sección \"módulo de cifrado básico\" de tu área de ajustes personales y actualiza tu contraseña de cifrado introduciendo esta contraseña en el campo \"contraseña de inicio de sesión antigua\" y su contraseña de inicio de sesión actual.\n\n", "The share will expire on %s." : "El archivo dejará de ser compartido el %s.", "Cheers!" : "¡Saludos!", diff --git a/apps/encryption/l10n/es.json b/apps/encryption/l10n/es.json index bbdd8fb296a..a6ddbc35eb1 100644 --- a/apps/encryption/l10n/es.json +++ b/apps/encryption/l10n/es.json @@ -30,6 +30,7 @@ "Cannot read this file, probably this is a shared file. Please ask the file owner to reshare the file with you." : "No se puede leer este archivo, probablemente se trate de un archivo compartido. Por favor, pida al propietario del archivo que vuelva a compartirlo con usted.", "Default encryption module" : "Módulo de cifrado por defecto", "Default encryption module for server-side encryption" : "Módulo de cifrado por defecto para el cifrado en el lado del servidor", + "In order to use this encryption module you need to enable server-side encryption in the admin settings. Once enabled this module will encrypt all your files transparently. The encryption is based on AES 256 keys.\nThe module will not touch existing files, only new files will be encrypted after server-side encryption was enabled. It is also not possible to disable the encryption again and switch back to an unencrypted system.\nPlease read the documentation to know all implications before you decide to enable server-side encryption." : "De manera de usar este módulo de cifrado necesitas activar el cifrado del lado del servidor en la configuraciones de administrador. Una vez que esté habilitado este módulo cifrará todos tus archivos de manera transparente. El cifrado está basado en llaves AES-256\nEl módulo no tocará los archivos existentes, solo los archivos nuevos serán cifrados una vez que el cifrado del lado del servidor se habilite. Además, no es posible deshabilitar el cifrado de nuevo y cambiar a un sistema sin cifrado.\nPor favor lea la documentación para que entienda todas las implicaciones antes de que decida habilitar el cifrado del lado del servidor.", "Hey there,\n\nThe administration 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" : "Hola,\n\nEl administrador habilitó el cifrado de datos del servidor. Tus archivos fueron cifrados con la contraseña \"%s\".\n\nPor favor, inicia sesión en la interfaz web, ve a la sección \"módulo de cifrado básico\" de tu área de ajustes personales y actualiza tu contraseña de cifrado introduciendo esta contraseña en el campo \"contraseña de inicio de sesión antigua\" y su contraseña de inicio de sesión actual.\n\n", "The share will expire on %s." : "El archivo dejará de ser compartido el %s.", "Cheers!" : "¡Saludos!", diff --git a/apps/encryption/lib/Crypto/Crypt.php b/apps/encryption/lib/Crypto/Crypt.php index fe9813a6cfa..22a697a1232 100644 --- a/apps/encryption/lib/Crypto/Crypt.php +++ b/apps/encryption/lib/Crypto/Crypt.php @@ -185,14 +185,9 @@ class Crypt { } /** - * @param string $plainContent - * @param string $passPhrase - * @param int $version - * @param int $position - * @return false|string * @throws EncryptionFailedException */ - public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) { + public function symmetricEncryptFileContent(string $plainContent, string $passPhrase, int $version, string $position): string|false { if (!$plainContent) { $this->logger->error('Encryption Library, symmetrical encryption failed no content given', ['app' => 'encryption']); @@ -409,7 +404,7 @@ class Crypt { $privateKey, $hash, 0, - 0 + '0' ); return $encryptedKey; @@ -537,12 +532,8 @@ class Crypt { /** * create signature - * - * @param string $data - * @param string $passPhrase - * @return string */ - private function createSignature($data, $passPhrase) { + private function createSignature(string $data, string $passPhrase): string { $passPhrase = hash('sha512', $passPhrase . 'a', true); return hash_hmac('sha256', $data, $passPhrase); } @@ -695,13 +686,25 @@ class Crypt { } /** - * @param string $encKeyFile - * @param string $shareKey * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey - * @return string * @throws MultiKeyDecryptException */ - public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) { + public function multiKeyDecrypt(string $shareKey, $privateKey): string { + $plainContent = ''; + + // decrypt the intermediate key with RSA + if (openssl_private_decrypt($shareKey, $intermediate, $privateKey, OPENSSL_PKCS1_OAEP_PADDING)) { + return $intermediate; + } else { + throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string()); + } + } + + /** + * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey + * @throws MultiKeyDecryptException + */ + public function multiKeyDecryptLegacy(string $encKeyFile, string $shareKey, $privateKey): string { if (!$encKeyFile) { throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content'); } @@ -715,12 +718,55 @@ class Crypt { } /** + * @param array<string,\OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string> $keyFiles + * @throws MultiKeyEncryptException + */ + public function multiKeyEncrypt(string $plainContent, array $keyFiles): array { + if (empty($plainContent)) { + throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content'); + } + + // Set empty vars to be set by openssl by reference + $shareKeys = []; + $mappedShareKeys = []; + + // make sure that there is at least one public key to use + if (count($keyFiles) >= 1) { + // prepare the encrypted keys + $shareKeys = []; + + // iterate over the public keys and encrypt the intermediate + // for each of them with RSA + foreach ($keyFiles as $tmp_key) { + if (openssl_public_encrypt($plainContent, $tmp_output, $tmp_key, OPENSSL_PKCS1_OAEP_PADDING)) { + $shareKeys[] = $tmp_output; + } + } + + // set the result if everything worked fine + if (count($keyFiles) === count($shareKeys)) { + $i = 0; + + // Ensure each shareKey is labelled with its corresponding key id + foreach ($keyFiles as $userId => $publicKey) { + $mappedShareKeys[$userId] = $shareKeys[$i]; + $i++; + } + + return $mappedShareKeys; + } + } + throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string()); + } + + /** * @param string $plainContent * @param array $keyFiles * @return array * @throws MultiKeyEncryptException + * @deprecated 27.0.0 use multiKeyEncrypt */ - public function multiKeyEncrypt($plainContent, array $keyFiles) { + public function multiKeyEncryptLegacy($plainContent, array $keyFiles) { // openssl_seal returns false without errors if plaincontent is empty // so trigger our own error if (empty($plainContent)) { @@ -809,6 +855,7 @@ class Crypt { /** * Custom implementation of openssl_seal() * + * @deprecated 27.0.0 use multiKeyEncrypt * @throws EncryptionFailedException */ private function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int|false { diff --git a/apps/encryption/lib/Crypto/Encryption.php b/apps/encryption/lib/Crypto/Encryption.php index b44472fd04a..465856fd28d 100644 --- a/apps/encryption/lib/Crypto/Encryption.php +++ b/apps/encryption/lib/Crypto/Encryption.php @@ -108,6 +108,8 @@ class Encryption implements IEncryptionModule { /** @var int Current version of the file */ private $version = 0; + private bool $useLegacyFileKey = true; + /** @var array remember encryption signature version */ private static $rememberVersion = []; @@ -182,6 +184,8 @@ class Encryption implements IEncryptionModule { $this->writeCache = ''; $this->useLegacyBase64Encoding = true; + $this->useLegacyFileKey = ($header['useLegacyFileKey'] ?? 'true') !== 'false'; + if (isset($header['encoding'])) { $this->useLegacyBase64Encoding = $header['encoding'] !== Crypt::BINARY_ENCODING_FORMAT; } @@ -195,13 +199,17 @@ class Encryption implements IEncryptionModule { } if ($this->session->decryptAllModeActivated()) { - $encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path); $shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid()); - $this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey, - $shareKey, - $this->session->getDecryptAllKey()); + if ($this->useLegacyFileKey) { + $encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path); + $this->fileKey = $this->crypt->multiKeyDecryptLegacy($encryptedFileKey, + $shareKey, + $this->session->getDecryptAllKey()); + } else { + $this->fileKey = $this->crypt->multiKeyDecrypt($shareKey, $this->session->getDecryptAllKey()); + } } else { - $this->fileKey = $this->keyManager->getFileKey($this->path, $this->user); + $this->fileKey = $this->keyManager->getFileKey($this->path, $this->user, $this->useLegacyFileKey); } // always use the version from the original file, also part files @@ -239,7 +247,11 @@ class Encryption implements IEncryptionModule { $this->cipher = $this->crypt->getLegacyCipher(); } - $result = ['cipher' => $this->cipher, 'signed' => 'true']; + $result = [ + 'cipher' => $this->cipher, + 'signed' => 'true', + 'useLegacyFileKey' => 'false', + ]; if ($this->useLegacyBase64Encoding !== true) { $result['encoding'] = Crypt::BINARY_ENCODING_FORMAT; @@ -254,14 +266,14 @@ class Encryption implements IEncryptionModule { * buffer. * * @param string $path to the file - * @param int $position + * @param string $position * @return string remained data which should be written to the file in case * of a write operation * @throws PublicKeyMissingException * @throws \Exception * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException */ - public function end($path, $position = 0) { + public function end($path, $position = '0') { $result = ''; if ($this->isWriteOperation) { // in case of a part file we remember the new signature versions @@ -296,10 +308,13 @@ class Encryption implements IEncryptionModule { } $publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->getOwner($path)); - $encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys); - $this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles); + $shareKeys = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys); + $this->keyManager->deleteLegacyFileKey($this->path); + foreach ($shareKeys as $uid => $keyFile) { + $this->keyManager->setShareKey($this->path, $uid, $keyFile); + } } - return $result; + return $result ?: ''; } @@ -315,7 +330,6 @@ class Encryption implements IEncryptionModule { // If extra data is left over from the last round, make sure it // is integrated into the next block if ($this->writeCache) { - // Concat writeCache to start of $data $data = $this->writeCache . $data; @@ -327,7 +341,6 @@ class Encryption implements IEncryptionModule { $encrypted = ''; // While there still remains some data to be processed & written while (strlen($data) > 0) { - // Remaining length for this iteration, not of the // entire file (may be greater than 8192 bytes) $remainingLength = strlen($data); @@ -335,7 +348,6 @@ class Encryption implements IEncryptionModule { // If data remaining to be written is less than the // size of 1 unencrypted block if ($remainingLength < $this->getUnencryptedBlockSize(true)) { - // Set writeCache to contents of $data // The writeCache will be carried over to the // next write round, and added to the start of @@ -349,11 +361,10 @@ class Encryption implements IEncryptionModule { // Clear $data ready for next round $data = ''; } else { - // Read the chunk from the start of $data $chunk = substr($data, 0, $this->getUnencryptedBlockSize(true)); - $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, $position); + $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, (string)$position); // Remove the chunk we just processed from // $data, leaving only unprocessed data in $data @@ -391,7 +402,7 @@ class Encryption implements IEncryptionModule { * @param string $path path to the file which should be updated * @param string $uid of the user who performs the operation * @param array $accessList who has access to the file contains the key 'users' and 'public' - * @return boolean + * @return bool */ public function update($path, $uid, array $accessList) { if (empty($accessList)) { @@ -399,10 +410,10 @@ class Encryption implements IEncryptionModule { $this->keyManager->setVersion($path, self::$rememberVersion[$path], new View()); unset(self::$rememberVersion[$path]); } - return; + return false; } - $fileKey = $this->keyManager->getFileKey($path, $uid); + $fileKey = $this->keyManager->getFileKey($path, $uid, null); if (!empty($fileKey)) { $publicKeys = []; @@ -420,11 +431,13 @@ class Encryption implements IEncryptionModule { $publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $this->getOwner($path)); - $encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys); + $shareKeys = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys); $this->keyManager->deleteAllFileKeys($path); - $this->keyManager->setAllFileKeys($path, $encryptedFileKey); + foreach ($shareKeys as $uid => $keyFile) { + $this->keyManager->setShareKey($this->path, $uid, $keyFile); + } } else { $this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted', ['file' => $path, 'app' => 'encryption']); @@ -503,7 +516,7 @@ class Encryption implements IEncryptionModule { * @throws DecryptionFailedException */ public function isReadable($path, $uid) { - $fileKey = $this->keyManager->getFileKey($path, $uid); + $fileKey = $this->keyManager->getFileKey($path, $uid, null); if (empty($fileKey)) { $owner = $this->util->getOwner($path); if ($owner !== $uid) { diff --git a/apps/encryption/lib/KeyManager.php b/apps/encryption/lib/KeyManager.php index 2c6487d062a..f0005b45761 100644 --- a/apps/encryption/lib/KeyManager.php +++ b/apps/encryption/lib/KeyManager.php @@ -44,7 +44,6 @@ use OCP\IUserSession; use OCP\Lock\ILockingProvider; class KeyManager { - /** * @var Session */ @@ -441,17 +440,21 @@ class KeyManager { /** * @param string $path * @param $uid + * @param ?bool $useLegacyFileKey null means try both * @return string */ - public function getFileKey($path, $uid) { + public function getFileKey(string $path, ?string $uid, ?bool $useLegacyFileKey): string { if ($uid === '') { $uid = null; } $publicAccess = is_null($uid); - $encryptedFileKey = $this->keyStorage->getFileKey($path, $this->fileKeyId, Encryption::ID); + $encryptedFileKey = ''; + if ($useLegacyFileKey ?? true) { + $encryptedFileKey = $this->keyStorage->getFileKey($path, $this->fileKeyId, Encryption::ID); - if (empty($encryptedFileKey)) { - return ''; + if (empty($encryptedFileKey) && $useLegacyFileKey) { + return ''; + } } if ($this->util->isMasterKeyEnabled()) { @@ -475,10 +478,17 @@ class KeyManager { $privateKey = $this->session->getPrivateKey(); } - if ($encryptedFileKey && $shareKey && $privateKey) { - return $this->crypt->multiKeyDecrypt($encryptedFileKey, - $shareKey, - $privateKey); + if ($useLegacyFileKey ?? true) { + if ($encryptedFileKey && $shareKey && $privateKey) { + return $this->crypt->multiKeyDecryptLegacy($encryptedFileKey, + $shareKey, + $privateKey); + } + } + if (!($useLegacyFileKey ?? false)) { + if ($shareKey && $privateKey) { + return $this->crypt->multiKeyDecrypt($shareKey, $privateKey); + } } return ''; @@ -656,6 +666,10 @@ class KeyManager { return $this->keyStorage->deleteAllFileKeys($path); } + public function deleteLegacyFileKey(string $path): bool { + return $this->keyStorage->deleteFileKey($path, $this->fileKeyId, Encryption::ID); + } + /** * @param array $userIds * @return array diff --git a/apps/encryption/lib/Recovery.php b/apps/encryption/lib/Recovery.php index f4336ec7c4e..25738dabf89 100644 --- a/apps/encryption/lib/Recovery.php +++ b/apps/encryption/lib/Recovery.php @@ -35,8 +35,6 @@ use OCP\IUserSession; use OCP\PreConditionNotMetException; class Recovery { - - /** * @var null|IUser */ @@ -102,7 +100,7 @@ class Recovery { } if ($keyManager->checkRecoveryPassword($password)) { - $appConfig->setAppValue('encryption', 'recoveryAdminEnabled', 1); + $appConfig->setAppValue('encryption', 'recoveryAdminEnabled', '1'); return true; } @@ -140,7 +138,7 @@ class Recovery { if ($keyManager->checkRecoveryPassword($recoveryPassword)) { // Set recoveryAdmin as disabled - $this->config->setAppValue('encryption', 'recoveryAdminEnabled', 0); + $this->config->setAppValue('encryption', 'recoveryAdminEnabled', '0'); return true; } return false; @@ -169,7 +167,7 @@ class Recovery { * @return bool */ public function isRecoveryKeyEnabled() { - $enabled = $this->config->getAppValue('encryption', 'recoveryAdminEnabled', 0); + $enabled = $this->config->getAppValue('encryption', 'recoveryAdminEnabled', '0'); return ($enabled === '1'); } @@ -199,16 +197,15 @@ class Recovery { /** * add recovery key to all encrypted files - * @param string $path */ - private function addRecoveryKeys($path) { + private function addRecoveryKeys(string $path): void { $dirContent = $this->view->getDirectoryContent($path); foreach ($dirContent as $item) { $filePath = $item->getPath(); if ($item['type'] === 'dir') { $this->addRecoveryKeys($filePath . '/'); } else { - $fileKey = $this->keyManager->getFileKey($filePath, $this->user->getUID()); + $fileKey = $this->keyManager->getFileKey($filePath, $this->user->getUID(), null); if (!empty($fileKey)) { $accessList = $this->file->getAccessList($filePath); $publicKeys = []; @@ -218,8 +215,11 @@ class Recovery { $publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $this->user->getUID()); - $encryptedKeyfiles = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys); - $this->keyManager->setAllFileKeys($filePath, $encryptedKeyfiles); + $shareKeys = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys); + $this->keyManager->deleteLegacyFileKey($filePath); + foreach ($shareKeys as $uid => $keyFile) { + $this->keyManager->setShareKey($filePath, $uid, $keyFile); + } } } } @@ -227,9 +227,8 @@ class Recovery { /** * remove recovery key to all encrypted files - * @param string $path */ - private function removeRecoveryKeys($path) { + private function removeRecoveryKeys(string $path): void { $dirContent = $this->view->getDirectoryContent($path); foreach ($dirContent as $item) { $filePath = $item->getPath(); @@ -243,11 +242,8 @@ class Recovery { /** * recover users files with the recovery key - * - * @param string $recoveryPassword - * @param string $user */ - public function recoverUsersFiles($recoveryPassword, $user) { + public function recoverUsersFiles(string $recoveryPassword, string $user): void { $encryptedKey = $this->keyManager->getSystemPrivateKey($this->keyManager->getRecoveryKeyId()); $privateKey = $this->crypt->decryptPrivateKey($encryptedKey, $recoveryPassword); @@ -258,12 +254,8 @@ class Recovery { /** * recover users files - * - * @param string $path - * @param string $privateKey - * @param string $uid */ - private function recoverAllFiles($path, $privateKey, $uid) { + private function recoverAllFiles(string $path, string $privateKey, string $uid): void { $dirContent = $this->view->getDirectoryContent($path); foreach ($dirContent as $item) { @@ -279,19 +271,17 @@ class Recovery { /** * recover file - * - * @param string $path - * @param string $privateKey - * @param string $uid */ - private function recoverFile($path, $privateKey, $uid) { + private function recoverFile(string $path, string $privateKey, string $uid): void { $encryptedFileKey = $this->keyManager->getEncryptedFileKey($path); $shareKey = $this->keyManager->getShareKey($path, $this->keyManager->getRecoveryKeyId()); if ($encryptedFileKey && $shareKey && $privateKey) { - $fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey, + $fileKey = $this->crypt->multiKeyDecryptLegacy($encryptedFileKey, $shareKey, $privateKey); + } elseif ($shareKey && $privateKey) { + $fileKey = $this->crypt->multiKeyDecrypt($shareKey, $privateKey); } if (!empty($fileKey)) { @@ -303,8 +293,11 @@ class Recovery { $publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $uid); - $encryptedKeyfiles = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys); - $this->keyManager->setAllFileKeys($path, $encryptedKeyfiles); + $shareKeys = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys); + $this->keyManager->deleteLegacyFileKey($path); + foreach ($shareKeys as $uid => $keyFile) { + $this->keyManager->setShareKey($path, $uid, $keyFile); + } } } } diff --git a/apps/encryption/tests/Command/FixEncryptedVersionTest.php b/apps/encryption/tests/Command/FixEncryptedVersionTest.php index 5c938b4350d..2a6c86ef5b2 100644 --- a/apps/encryption/tests/Command/FixEncryptedVersionTest.php +++ b/apps/encryption/tests/Command/FixEncryptedVersionTest.php @@ -263,7 +263,7 @@ Fixed the file: \"/$this->userId/files/world.txt\" with version 4", $output); $cacheInfo = ['encryptedVersion' => 1, 'encrypted' => 1]; $cache1->put($fileCache1->getPath(), $cacheInfo); - $absPath = $view->getLocalFolder(''). '/hello.txt'; + $absPath = $storage1->getSourcePath('').$fileInfo1->getInternalPath(); // create unencrypted file on disk, the version stays file_put_contents($absPath, 'hello contents'); diff --git a/apps/encryption/tests/Crypto/CryptTest.php b/apps/encryption/tests/Crypto/CryptTest.php index 08d0bba2668..dd41c67e8ad 100644 --- a/apps/encryption/tests/Crypto/CryptTest.php +++ b/apps/encryption/tests/Crypto/CryptTest.php @@ -34,8 +34,6 @@ use OCP\IUserSession; use Test\TestCase; class CryptTest extends TestCase { - - /** @var \OCP\ILogger|\PHPUnit\Framework\MockObject\MockObject */ private $logger; @@ -155,7 +153,7 @@ class CryptTest extends TestCase { ->method('warning') ->with('Unsupported cipher (Not-Existing-Cipher) defined in config.php supported. Falling back to AES-256-CTR'); - $this->assertSame('AES-256-CTR', $this->crypt->getCipher()); + $this->assertSame('AES-256-CTR', $this->crypt->getCipher()); } /** @@ -396,7 +394,7 @@ class CryptTest extends TestCase { public function testDecryptPrivateKey($header, $privateKey, $expectedCipher, $isValidKey, $expected) { $this->config->method('getSystemValueBool') ->withConsecutive(['encryption.legacy_format_support', false], - ['encryption.use_legacy_base64_encoding', false]) + ['encryption.use_legacy_base64_encoding', false]) ->willReturnOnConsecutiveCalls(true, false); /** @var \OCA\Encryption\Crypto\Crypt | \PHPUnit\Framework\MockObject\MockObject $crypt */ @@ -465,4 +463,17 @@ class CryptTest extends TestCase { $this->invokePrivate($this->crypt, 'isValidPrivateKey', ['foo']) ); } + + public function testMultiKeyEncrypt() { + $res = openssl_pkey_new(); + openssl_pkey_export($res, $privateKey); + $publicKeyPem = openssl_pkey_get_details($res)['key']; + $publicKey = openssl_pkey_get_public($publicKeyPem); + + $shareKeys = $this->crypt->multiKeyEncrypt('content', ['user1' => $publicKey]); + $this->assertEquals( + 'content', + $this->crypt->multiKeyDecrypt($shareKeys['user1'], $privateKey) + ); + } } diff --git a/apps/encryption/tests/Crypto/EncryptionTest.php b/apps/encryption/tests/Crypto/EncryptionTest.php index 63698dcdc63..675a8bf8e29 100644 --- a/apps/encryption/tests/Crypto/EncryptionTest.php +++ b/apps/encryption/tests/Crypto/EncryptionTest.php @@ -42,7 +42,6 @@ use Symfony\Component\Console\Output\OutputInterface; use Test\TestCase; class EncryptionTest extends TestCase { - /** @var Encryption */ private $instance; @@ -156,7 +155,7 @@ class EncryptionTest extends TestCase { ->willReturnCallback([$this, 'addSystemKeysCallback']); $this->cryptMock->expects($this->any()) ->method('multiKeyEncrypt') - ->willReturn(true); + ->willReturn([]); $this->instance->end('/foo/bar'); } @@ -276,7 +275,7 @@ class EncryptionTest extends TestCase { ->with($path, $recoveryKeyId) ->willReturn($recoveryShareKey); $this->cryptMock->expects($this->once()) - ->method('multiKeyDecrypt') + ->method('multiKeyDecryptLegacy') ->with('encryptedFileKey', $recoveryShareKey, $decryptAllKey) ->willReturn($fileKey); @@ -378,6 +377,7 @@ class EncryptionTest extends TestCase { function ($fileKey, $publicKeys) { $this->assertEmpty($publicKeys); $this->assertSame('fileKey', $fileKey); + return []; } ); diff --git a/apps/encryption/tests/KeyManagerTest.php b/apps/encryption/tests/KeyManagerTest.php index c08f8b576d9..be116318185 100644 --- a/apps/encryption/tests/KeyManagerTest.php +++ b/apps/encryption/tests/KeyManagerTest.php @@ -267,7 +267,6 @@ class KeyManagerTest extends TestCase { * @param bool $useMasterKey */ public function testInit($useMasterKey) { - /** @var \OCA\Encryption\KeyManager|\PHPUnit\Framework\MockObject\MockObject $instance */ $instance = $this->getMockBuilder(KeyManager::class) ->setConstructorArgs( @@ -373,14 +372,22 @@ class KeyManagerTest extends TestCase { public function dataTestGetFileKey() { return [ - ['user1', false, 'privateKey', true], - ['user1', false, false, ''], - ['user1', true, 'privateKey', true], - ['user1', true, false, ''], - [null, false, 'privateKey', true], - [null, false, false, ''], - [null, true, 'privateKey', true], - [null, true, false, ''] + ['user1', false, 'privateKey', 'legacyKey', 'multiKeyDecryptResult'], + ['user1', false, 'privateKey', '', 'multiKeyDecryptResult'], + ['user1', false, false, 'legacyKey', ''], + ['user1', false, false, '', ''], + ['user1', true, 'privateKey', 'legacyKey', 'multiKeyDecryptResult'], + ['user1', true, 'privateKey', '', 'multiKeyDecryptResult'], + ['user1', true, false, 'legacyKey', ''], + ['user1', true, false, '', ''], + [null, false, 'privateKey', 'legacyKey', 'multiKeyDecryptResult'], + [null, false, 'privateKey', '', 'multiKeyDecryptResult'], + [null, false, false, 'legacyKey', ''], + [null, false, false, '', ''], + [null, true, 'privateKey', 'legacyKey', 'multiKeyDecryptResult'], + [null, true, 'privateKey', '', 'multiKeyDecryptResult'], + [null, true, false, 'legacyKey', ''], + [null, true, false, '', ''], ]; } @@ -392,7 +399,7 @@ class KeyManagerTest extends TestCase { * @param $privateKey * @param $expected */ - public function testGetFileKey($uid, $isMasterKeyEnabled, $privateKey, $expected) { + public function testGetFileKey($uid, $isMasterKeyEnabled, $privateKey, $encryptedFileKey, $expected) { $path = '/foo.txt'; if ($isMasterKeyEnabled) { @@ -414,8 +421,8 @@ class KeyManagerTest extends TestCase { [$path, $expectedUid . '.shareKey', 'OC_DEFAULT_MODULE'], ) ->willReturnOnConsecutiveCalls( - true, - true, + $encryptedFileKey, + 'fileKey', ); $this->utilMock->expects($this->any())->method('isMasterKeyEnabled') @@ -434,17 +441,32 @@ class KeyManagerTest extends TestCase { $this->sessionMock->expects($this->once())->method('getPrivateKey')->willReturn($privateKey); } - if ($privateKey) { - $this->cryptMock->expects($this->once()) - ->method('multiKeyDecrypt') - ->willReturn(true); - } else { + if (!empty($encryptedFileKey)) { $this->cryptMock->expects($this->never()) ->method('multiKeyDecrypt'); + if ($privateKey) { + $this->cryptMock->expects($this->once()) + ->method('multiKeyDecryptLegacy') + ->willReturn('multiKeyDecryptResult'); + } else { + $this->cryptMock->expects($this->never()) + ->method('multiKeyDecryptLegacy'); + } + } else { + $this->cryptMock->expects($this->never()) + ->method('multiKeyDecryptLegacy'); + if ($privateKey) { + $this->cryptMock->expects($this->once()) + ->method('multiKeyDecrypt') + ->willReturn('multiKeyDecryptResult'); + } else { + $this->cryptMock->expects($this->never()) + ->method('multiKeyDecrypt'); + } } $this->assertSame($expected, - $this->instance->getFileKey($path, $uid) + $this->instance->getFileKey($path, $uid, null) ); } @@ -562,7 +584,6 @@ class KeyManagerTest extends TestCase { * @param $masterKey */ public function testValidateMasterKey($masterKey) { - /** @var \OCA\Encryption\KeyManager | \PHPUnit\Framework\MockObject\MockObject $instance */ $instance = $this->getMockBuilder(KeyManager::class) ->setConstructorArgs( diff --git a/apps/encryption/tests/RecoveryTest.php b/apps/encryption/tests/RecoveryTest.php index 37b6671d5cb..af053515f8c 100644 --- a/apps/encryption/tests/RecoveryTest.php +++ b/apps/encryption/tests/RecoveryTest.php @@ -211,7 +211,8 @@ class RecoveryTest extends TestCase { ->willReturn([]); $this->cryptMock->expects($this->once()) - ->method('decryptPrivateKey'); + ->method('decryptPrivateKey') + ->willReturn('privateKey'); $this->instance->recoverUsersFiles('password', 'admin'); $this->addToAssertionCount(1); } @@ -226,8 +227,8 @@ class RecoveryTest extends TestCase { ->willReturn(true); $this->cryptMock->expects($this->once()) - ->method('multiKeyDecrypt') - ->willReturn(true); + ->method('multiKeyDecryptLegacy') + ->willReturn('multiKeyDecryptLegacyResult'); $this->fileMock->expects($this->once()) ->method('getAccessList') @@ -244,10 +245,13 @@ class RecoveryTest extends TestCase { $this->cryptMock->expects($this->once()) - ->method('multiKeyEncrypt'); + ->method('multiKeyEncrypt') + ->willReturn(['admin' => 'shareKey']); $this->keyManagerMock->expects($this->once()) - ->method('setAllFileKeys'); + ->method('deleteLegacyFileKey'); + $this->keyManagerMock->expects($this->once()) + ->method('setShareKey'); $this->assertNull(self::invokePrivate($this->instance, 'recoverFile', diff --git a/apps/files/js/breadcrumb.js b/apps/files/js/breadcrumb.js index 1083be461bb..ed1170b0f59 100644 --- a/apps/files/js/breadcrumb.js +++ b/apps/files/js/breadcrumb.js @@ -32,7 +32,7 @@ * the URL of a given breadcrumb */ var BreadCrumb = function(options){ - this.$el = $('<div class="breadcrumb"></div>'); + this.$el = $('<nav></nav>'); this.$menu = $('<div class="popovermenu menu-center"><ul></ul></div>'); this.crumbSelector = '.crumb:not(.hidden):not(.crumbhome):not(.crumbmenu)'; @@ -123,12 +123,13 @@ var $menuItem; this.$el.empty(); this.breadcrumbs = []; + var $crumbList = $('<ul class="breadcrumb"></ul>'); for (var i = 0; i < parts.length; i++) { var part = parts[i]; var $image; var $link = $('<a></a>'); - $crumb = $('<div class="crumb svg"></div>'); + $crumb = $('<li class="crumb svg"></li>'); if(part.dir) { $link.attr('href', this.getCrumbUrl(part, i)); } @@ -149,12 +150,13 @@ $link.append($image); } this.breadcrumbs.push($crumb); - this.$el.append($crumb); + $crumbList.append($crumb); // Only add feedback if not menu if (this.onClick && i !== 0) { $link.on('click', this.onClick); } } + this.$el.append($crumbList); // Menu creation this._createMenu(); diff --git a/apps/files/l10n/pl.js b/apps/files/l10n/pl.js index e89a241088d..a544127754b 100644 --- a/apps/files/l10n/pl.js +++ b/apps/files/l10n/pl.js @@ -185,13 +185,17 @@ OC.L10N.register( "Select file or folder to link to" : "Wybierz plik lub katalog do linku", "Open the files app settings" : "Otwórz ustawienia aplikacji plików", "Files settings" : "Ustawienia Plików", + "File cannot be accessed" : "Nie można uzyskać dostępu do pliku", + "You might not have have permissions to view it, ask the sender to share it" : "Możesz nie mieć uprawnień, aby go wyświetlić, poproś nadawcę o udostępnienie", "Show hidden files" : "Pokaż ukryte pliki", "Crop image previews" : "Przytnij podglądy obrazów", "Additional settings" : "Ustawienia dodatkowe", "WebDAV" : "WebDAV", "Copy to clipboard" : "Kopiuj do schowka", "Use this address to access your Files via WebDAV" : "Użyj tego adresu, aby uzyskać dostęp do plików poprzez WebDAV", + "If you have enabled 2FA, you must create and use a new app password by clicking here." : "Jeśli włączyłeś 2FA, musisz utworzyć i używać nowego hasła do aplikacji, klikając tutaj.", "Clipboard is not available" : "Schowek jest niedostępny", + "WebDAV URL copied to clipboard" : "Adres URL WebDAV skopiowany do schowka", "Unable to change the favourite state of the file" : "Nie można zmienić ulubionego stanu pliku", "Error while loading the file data" : "Błąd podczas ładowania danych pliku", "Pick a template for {name}" : "Wybierz szablon dla {name}", diff --git a/apps/files/l10n/pl.json b/apps/files/l10n/pl.json index 58f2a89ffdd..b19568e2647 100644 --- a/apps/files/l10n/pl.json +++ b/apps/files/l10n/pl.json @@ -183,13 +183,17 @@ "Select file or folder to link to" : "Wybierz plik lub katalog do linku", "Open the files app settings" : "Otwórz ustawienia aplikacji plików", "Files settings" : "Ustawienia Plików", + "File cannot be accessed" : "Nie można uzyskać dostępu do pliku", + "You might not have have permissions to view it, ask the sender to share it" : "Możesz nie mieć uprawnień, aby go wyświetlić, poproś nadawcę o udostępnienie", "Show hidden files" : "Pokaż ukryte pliki", "Crop image previews" : "Przytnij podglądy obrazów", "Additional settings" : "Ustawienia dodatkowe", "WebDAV" : "WebDAV", "Copy to clipboard" : "Kopiuj do schowka", "Use this address to access your Files via WebDAV" : "Użyj tego adresu, aby uzyskać dostęp do plików poprzez WebDAV", + "If you have enabled 2FA, you must create and use a new app password by clicking here." : "Jeśli włączyłeś 2FA, musisz utworzyć i używać nowego hasła do aplikacji, klikając tutaj.", "Clipboard is not available" : "Schowek jest niedostępny", + "WebDAV URL copied to clipboard" : "Adres URL WebDAV skopiowany do schowka", "Unable to change the favourite state of the file" : "Nie można zmienić ulubionego stanu pliku", "Error while loading the file data" : "Błąd podczas ładowania danych pliku", "Pick a template for {name}" : "Wybierz szablon dla {name}", diff --git a/apps/files/tests/js/breadcrumbSpec.js b/apps/files/tests/js/breadcrumbSpec.js index 52c9f51835e..6bc7eb0a54d 100644 --- a/apps/files/tests/js/breadcrumbSpec.js +++ b/apps/files/tests/js/breadcrumbSpec.js @@ -41,7 +41,7 @@ describe('OCA.Files.BreadCrumb tests', function() { }); it('Renders its own container', function() { bc.render(); - expect(bc.$el.hasClass('breadcrumb')).toEqual(true); + expect(bc.$el.find("ul").hasClass('breadcrumb')).toEqual(true); }); it('Renders root by default', function() { var $crumbs; @@ -189,7 +189,7 @@ describe('OCA.Files.BreadCrumb tests', function() { bc.setDirectory(dummyDir); - $('div.crumb').each(function(index){ + $('li.crumb').each(function(index){ $(this).css('width', 50); $(this).css('padding', 0); $(this).css('margin', 0); @@ -248,7 +248,7 @@ describe('OCA.Files.BreadCrumb tests', function() { paddings = [0, 0, 0, 0, 0, 0, 0, 0]; margins = [0, 0, 0, 0, 0, 0, 0, 0]; - $('div.crumb').each(function(index){ + $('li.crumb').each(function(index){ $(this).css('width', widths[index]); $(this).css('padding', paddings[index]); $(this).css('margin', margins[index]); @@ -302,7 +302,7 @@ describe('OCA.Files.BreadCrumb tests', function() { // Each element is 20px wider paddings = [10, 10, 10, 10, 10, 10, 10, 10]; - $('div.crumb').each(function(index){ + $('li.crumb').each(function(index){ $(this).css('padding', paddings[index]); }); @@ -329,7 +329,7 @@ describe('OCA.Files.BreadCrumb tests', function() { // Each element is 20px wider margins = [10, 10, 10, 10, 10, 10, 10, 10]; - $('div.crumb').each(function(index){ + $('li.crumb').each(function(index){ $(this).css('margin', margins[index]); }); diff --git a/apps/files_external/l10n/es_EC.js b/apps/files_external/l10n/es_EC.js index 9c46ee0650e..3c8720800a7 100644 --- a/apps/files_external/l10n/es_EC.js +++ b/apps/files_external/l10n/es_EC.js @@ -22,6 +22,7 @@ OC.L10N.register( "Admin defined" : "Administrador definido", "Delete storage?" : "¿Borrar almacenamiento?", "Saved" : "Guardado", + "Saving …" : "Saving …", "Save" : "Guardar", "Empty response from the server" : "Respuesta del servidor vacía", "Couldn't access. Please log out and in again to activate this mount point" : "No fue posible accesar. Por favor sal de la sesión y vuelve a entrar para activar este punto de montaje", diff --git a/apps/files_external/l10n/es_EC.json b/apps/files_external/l10n/es_EC.json index 3ca6c1ae360..85f78621931 100644 --- a/apps/files_external/l10n/es_EC.json +++ b/apps/files_external/l10n/es_EC.json @@ -20,6 +20,7 @@ "Admin defined" : "Administrador definido", "Delete storage?" : "¿Borrar almacenamiento?", "Saved" : "Guardado", + "Saving …" : "Saving …", "Save" : "Guardar", "Empty response from the server" : "Respuesta del servidor vacía", "Couldn't access. Please log out and in again to activate this mount point" : "No fue posible accesar. Por favor sal de la sesión y vuelve a entrar para activar este punto de montaje", diff --git a/apps/files_sharing/src/style/sharebreadcrumb.scss b/apps/files_sharing/src/style/sharebreadcrumb.scss index f3096f45013..c4853e05165 100644 --- a/apps/files_sharing/src/style/sharebreadcrumb.scss +++ b/apps/files_sharing/src/style/sharebreadcrumb.scss @@ -20,15 +20,15 @@ * */ -div.crumb span.icon-shared, -div.crumb span.icon-public { +li.crumb span.icon-shared, +li.crumb span.icon-public { display: inline-block; cursor: pointer; opacity: 0.2; margin-right: 6px; } -div.crumb span.icon-shared.shared, -div.crumb span.icon-public.shared { +li.crumb span.icon-shared.shared, +li.crumb span.icon-public.shared { opacity: 0.7; } diff --git a/apps/files_trashbin/lib/Command/CleanUp.php b/apps/files_trashbin/lib/Command/CleanUp.php index e3ed527e535..a00a96d5ee2 100644 --- a/apps/files_trashbin/lib/Command/CleanUp.php +++ b/apps/files_trashbin/lib/Command/CleanUp.php @@ -78,13 +78,14 @@ class CleanUp extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $users = $input->getArgument('user_id'); + $verbose = $input->getOption('verbose'); if ((!empty($users)) and ($input->getOption('all-users'))) { throw new InvalidOptionException('Either specify a user_id or --all-users'); } elseif (!empty($users)) { foreach ($users as $user) { if ($this->userManager->userExists($user)) { $output->writeln("Remove deleted files of <info>$user</info>"); - $this->removeDeletedFiles($user); + $this->removeDeletedFiles($user, $output, $verbose); } else { $output->writeln("<error>Unknown user $user</error>"); return 1; @@ -104,7 +105,7 @@ class CleanUp extends Command { $users = $backend->getUsers('', $limit, $offset); foreach ($users as $user) { $output->writeln(" <info>$user</info>"); - $this->removeDeletedFiles($user); + $this->removeDeletedFiles($user, $output, $verbose); } $offset += $limit; } while (count($users) >= $limit); @@ -117,19 +118,31 @@ class CleanUp extends Command { /** * remove deleted files for the given user - * - * @param string $uid */ - protected function removeDeletedFiles($uid) { + protected function removeDeletedFiles(string $uid, OutputInterface $output, bool $verbose): void { \OC_Util::tearDownFS(); \OC_Util::setupFS($uid); - if ($this->rootFolder->nodeExists('/' . $uid . '/files_trashbin')) { - $this->rootFolder->get('/' . $uid . '/files_trashbin')->delete(); + $path = '/' . $uid . '/files_trashbin'; + if ($this->rootFolder->nodeExists($path)) { + $node = $this->rootFolder->get($path); + + if ($verbose) { + $output->writeln("Deleting <info>" . \OC_Helper::humanFileSize($node->getSize()) . "</info> in trash for <info>$uid</info>."); + } + $node->delete(); + if ($this->rootFolder->nodeExists($path)) { + $output->writeln("<error>Trash folder sill exists after attempting to delete it</error>"); + return; + } $query = $this->dbConnection->getQueryBuilder(); $query->delete('files_trash') ->where($query->expr()->eq('user', $query->createParameter('uid'))) ->setParameter('uid', $uid); $query->execute(); + } else { + if ($verbose) { + $output->writeln("No trash found for <info>$uid</info>"); + } } } } diff --git a/apps/files_trashbin/tests/Command/CleanUpTest.php b/apps/files_trashbin/tests/Command/CleanUpTest.php index 949d0529fd6..2f582ed428b 100644 --- a/apps/files_trashbin/tests/Command/CleanUpTest.php +++ b/apps/files_trashbin/tests/Command/CleanUpTest.php @@ -32,6 +32,7 @@ use OCP\Files\IRootFolder; use OCP\IDBConnection; use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Test\TestCase; @@ -101,27 +102,27 @@ class CleanUpTest extends TestCase { * @dataProvider dataTestRemoveDeletedFiles * @param boolean $nodeExists */ - public function testRemoveDeletedFiles($nodeExists) { + public function testRemoveDeletedFiles(bool $nodeExists) { $this->initTable(); - $this->rootFolder->expects($this->once()) + $this->rootFolder ->method('nodeExists') ->with('/' . $this->user0 . '/files_trashbin') - ->willReturn($nodeExists); + ->willReturnOnConsecutiveCalls($nodeExists, false); if ($nodeExists) { - $this->rootFolder->expects($this->once()) + $this->rootFolder ->method('get') ->with('/' . $this->user0 . '/files_trashbin') ->willReturn($this->rootFolder); - $this->rootFolder->expects($this->once()) + $this->rootFolder ->method('delete'); } else { $this->rootFolder->expects($this->never())->method('get'); $this->rootFolder->expects($this->never())->method('delete'); } - $this->invokePrivate($this->cleanup, 'removeDeletedFiles', [$this->user0]); + $this->invokePrivate($this->cleanup, 'removeDeletedFiles', [$this->user0, new NullOutput(), false]); if ($nodeExists) { - // if the delete operation was execute only files from user1 + // if the delete operation was executed only files from user1 // should be left. $query = $this->dbConnection->getQueryBuilder(); $query->select('user') @@ -136,7 +137,7 @@ class CleanUpTest extends TestCase { $this->assertSame('user1', $r['user']); } } else { - // if no delete operation was execute we should still have all 10 + // if no delete operation was executed we should still have all 10 // database entries $getAllQuery = $this->dbConnection->getQueryBuilder(); $result = $getAllQuery->select('id') @@ -171,9 +172,14 @@ class CleanUpTest extends TestCase { ->method('userExists')->willReturn(true); $inputInterface = $this->getMockBuilder('\Symfony\Component\Console\Input\InputInterface') ->disableOriginalConstructor()->getMock(); - $inputInterface->expects($this->once())->method('getArgument') + $inputInterface->method('getArgument') ->with('user_id') ->willReturn($userIds); + $inputInterface->method('getOption') + ->willReturnMap([ + ['all-users', false], + ['verbose', false], + ]); $outputInterface = $this->getMockBuilder('\Symfony\Component\Console\Output\OutputInterface') ->disableOriginalConstructor()->getMock(); $this->invokePrivate($instance, 'execute', [$inputInterface, $outputInterface]); @@ -190,7 +196,7 @@ class CleanUpTest extends TestCase { ->setConstructorArgs([$this->rootFolder, $this->userManager, $this->dbConnection]) ->getMock(); $backend = $this->createMock(\OCP\UserInterface::class); - $backend->expects($this->once())->method('getUsers') + $backend->method('getUsers') ->with('', 500, 0) ->willReturn($backendUsers); $instance->expects($this->exactly(count($backendUsers))) @@ -199,14 +205,16 @@ class CleanUpTest extends TestCase { $this->assertTrue(in_array($user, $backendUsers)); }); $inputInterface = $this->createMock(InputInterface::class); - $inputInterface->expects($this->once())->method('getArgument') + $inputInterface->method('getArgument') ->with('user_id') ->willReturn($userIds); $inputInterface->method('getOption') - ->with('all-users') - ->willReturn(true); + ->willReturnMap([ + ['all-users', true], + ['verbose', false], + ]); $outputInterface = $this->createMock(OutputInterface::class); - $this->userManager->expects($this->once()) + $this->userManager ->method('getBackends') ->willReturn([$backend]); $this->invokePrivate($instance, 'execute', [$inputInterface, $outputInterface]); @@ -214,12 +222,14 @@ class CleanUpTest extends TestCase { public function testExecuteNoUsersAndNoAllUsers() { $inputInterface = $this->createMock(InputInterface::class); - $inputInterface->expects($this->once())->method('getArgument') + $inputInterface->method('getArgument') ->with('user_id') ->willReturn([]); $inputInterface->method('getOption') - ->with('all-users') - ->willReturn(false); + ->willReturnMap([ + ['all-users', false], + ['verbose', false], + ]); $outputInterface = $this->createMock(OutputInterface::class); $this->expectException(InvalidOptionException::class); @@ -230,12 +240,14 @@ class CleanUpTest extends TestCase { public function testExecuteUsersAndAllUsers() { $inputInterface = $this->createMock(InputInterface::class); - $inputInterface->expects($this->once())->method('getArgument') + $inputInterface->method('getArgument') ->with('user_id') ->willReturn(['user1', 'user2']); $inputInterface->method('getOption') - ->with('all-users') - ->willReturn(true); + ->willReturnMap([ + ['all-users', true], + ['verbose', false], + ]); $outputInterface = $this->createMock(OutputInterface::class); $this->expectException(InvalidOptionException::class); diff --git a/apps/files_versions/lib/Versions/IVersionBackend.php b/apps/files_versions/lib/Versions/IVersionBackend.php index c06e395e4c1..39e75672266 100644 --- a/apps/files_versions/lib/Versions/IVersionBackend.php +++ b/apps/files_versions/lib/Versions/IVersionBackend.php @@ -78,7 +78,7 @@ interface IVersionBackend { * Open the file for reading * * @param IVersion $version - * @return resource + * @return resource|false * @throws NotFoundException * @since 15.0.0 */ diff --git a/apps/settings/l10n/pl.js b/apps/settings/l10n/pl.js index 42ddf779932..c899b51e641 100644 --- a/apps/settings/l10n/pl.js +++ b/apps/settings/l10n/pl.js @@ -268,8 +268,10 @@ OC.L10N.register( "Current password" : "Bieżące hasło", "New password" : "Nowe hasło", "Change password" : "Zmień hasło", + "Your profile information" : "Informacje o Twoim profilu", "Your profile picture" : "Twoje zdjęcie profilowe", "Upload profile picture" : "Wyślij zdjęcie profilowe", + "Choose profile picture from Files" : "Wybierz zdjęcie profilowe z Plików", "Remove profile picture" : "Usuń zdjęcie profilowe", "png or jpg, max. 20 MB" : "png lub jpg, maks. 20 MB", "Picture provided by original account" : "Zdjęcie dostarczone przez oryginalne konto", diff --git a/apps/settings/l10n/pl.json b/apps/settings/l10n/pl.json index 12570d52839..0da58ef445c 100644 --- a/apps/settings/l10n/pl.json +++ b/apps/settings/l10n/pl.json @@ -266,8 +266,10 @@ "Current password" : "Bieżące hasło", "New password" : "Nowe hasło", "Change password" : "Zmień hasło", + "Your profile information" : "Informacje o Twoim profilu", "Your profile picture" : "Twoje zdjęcie profilowe", "Upload profile picture" : "Wyślij zdjęcie profilowe", + "Choose profile picture from Files" : "Wybierz zdjęcie profilowe z Plików", "Remove profile picture" : "Usuń zdjęcie profilowe", "png or jpg, max. 20 MB" : "png lub jpg, maks. 20 MB", "Picture provided by original account" : "Zdjęcie dostarczone przez oryginalne konto", diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php index 82a7e118d06..81a112cac55 100644 --- a/apps/settings/lib/Controller/CheckSetupController.php +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -632,7 +632,7 @@ Raw output } protected function getLastCronInfo(): array { - $lastCronRun = $this->config->getAppValue('core', 'lastcron', 0); + $lastCronRun = (int)$this->config->getAppValue('core', 'lastcron', '0'); return [ 'diffInSeconds' => time() - $lastCronRun, 'relativeTime' => $this->dateTimeFormatter->formatTimeSpan($lastCronRun), diff --git a/apps/settings/lib/Settings/Admin/Sharing.php b/apps/settings/lib/Settings/Admin/Sharing.php index d3c6839b8f7..32b09f333a9 100644 --- a/apps/settings/lib/Settings/Admin/Sharing.php +++ b/apps/settings/lib/Settings/Admin/Sharing.php @@ -104,7 +104,7 @@ class Sharing implements IDelegatedSettings { 'shareExcludedGroupsList' => $excludeGroupsList, 'publicShareDisclaimerText' => $this->config->getAppValue('core', 'shareapi_public_link_disclaimertext', null), 'enableLinkPasswordByDefault' => $this->config->getAppValue('core', 'shareapi_enable_link_password_by_default', 'no'), - 'shareApiDefaultPermissions' => $this->config->getAppValue('core', 'shareapi_default_permissions', Constants::PERMISSION_ALL), + 'shareApiDefaultPermissions' => (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL), 'shareApiDefaultPermissionsCheckboxes' => $this->getSharePermissionList(), 'shareDefaultInternalExpireDateSet' => $this->config->getAppValue('core', 'shareapi_default_internal_expire_date', 'no'), 'shareInternalExpireAfterNDays' => $this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7'), diff --git a/apps/settings/tests/Settings/Admin/SharingTest.php b/apps/settings/tests/Settings/Admin/SharingTest.php index 2468ad97d1e..c0b5861f549 100644 --- a/apps/settings/tests/Settings/Admin/SharingTest.php +++ b/apps/settings/tests/Settings/Admin/SharingTest.php @@ -93,7 +93,7 @@ class SharingTest extends TestCase { ['core', 'shareapi_exclude_groups', 'no', 'no'], ['core', 'shareapi_public_link_disclaimertext', null, 'Lorem ipsum'], ['core', 'shareapi_enable_link_password_by_default', 'no', 'yes'], - ['core', 'shareapi_default_permissions', Constants::PERMISSION_ALL, Constants::PERMISSION_ALL], + ['core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL, Constants::PERMISSION_ALL], ['core', 'shareapi_default_internal_expire_date', 'no', 'no'], ['core', 'shareapi_internal_expire_after_n_days', '7', '7'], ['core', 'shareapi_enforce_internal_expire_date', 'no', 'no'], @@ -175,7 +175,7 @@ class SharingTest extends TestCase { ['core', 'shareapi_exclude_groups', 'no', 'yes'], ['core', 'shareapi_public_link_disclaimertext', null, 'Lorem ipsum'], ['core', 'shareapi_enable_link_password_by_default', 'no', 'yes'], - ['core', 'shareapi_default_permissions', Constants::PERMISSION_ALL, Constants::PERMISSION_ALL], + ['core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL, Constants::PERMISSION_ALL], ['core', 'shareapi_default_internal_expire_date', 'no', 'no'], ['core', 'shareapi_internal_expire_after_n_days', '7', '7'], ['core', 'shareapi_enforce_internal_expire_date', 'no', 'no'], diff --git a/apps/updatenotification/l10n/es.js b/apps/updatenotification/l10n/es.js index 5ed461a0448..a7c3c240153 100644 --- a/apps/updatenotification/l10n/es.js +++ b/apps/updatenotification/l10n/es.js @@ -29,6 +29,7 @@ OC.L10N.register( "Update channel:" : "Canal de actualización: ", "You can always update to a newer version. But you can never downgrade to a more stable version." : "Siempre puedes actualizar a una versión más reciente. Pero no podrás desactualizar a la versión anterior.", "Notify members of the following groups about available updates:" : "Notificar a los miembros de los siguientes grupos sobre actualizaciones disponibles:", + "No groups" : "Sin grupos", "Only notifications for app updates are available." : "Solo están disponibles las notificaciones para actualizaciones de apps.", "The selected update channel makes dedicated notifications for the server obsolete." : "El canal de actualización seleccionado hace obsoletas las notificaciones dedicadas para el servidor.", "The selected update channel does not support updates of the server." : "El canal de actualización seleccionado no soporta actualizaciones del servidor", diff --git a/apps/updatenotification/l10n/es.json b/apps/updatenotification/l10n/es.json index 03d7254fbcc..fe1657c2c6d 100644 --- a/apps/updatenotification/l10n/es.json +++ b/apps/updatenotification/l10n/es.json @@ -27,6 +27,7 @@ "Update channel:" : "Canal de actualización: ", "You can always update to a newer version. But you can never downgrade to a more stable version." : "Siempre puedes actualizar a una versión más reciente. Pero no podrás desactualizar a la versión anterior.", "Notify members of the following groups about available updates:" : "Notificar a los miembros de los siguientes grupos sobre actualizaciones disponibles:", + "No groups" : "Sin grupos", "Only notifications for app updates are available." : "Solo están disponibles las notificaciones para actualizaciones de apps.", "The selected update channel makes dedicated notifications for the server obsolete." : "El canal de actualización seleccionado hace obsoletas las notificaciones dedicadas para el servidor.", "The selected update channel does not support updates of the server." : "El canal de actualización seleccionado no soporta actualizaciones del servidor", diff --git a/apps/updatenotification/lib/Controller/AdminController.php b/apps/updatenotification/lib/Controller/AdminController.php index b13ba66efd5..74a3a86c7e1 100644 --- a/apps/updatenotification/lib/Controller/AdminController.php +++ b/apps/updatenotification/lib/Controller/AdminController.php @@ -85,7 +85,7 @@ class AdminController extends Controller { */ public function setChannel(string $channel): DataResponse { Util::setChannel($channel); - $this->config->setAppValue('core', 'lastupdatedat', 0); + $this->config->setAppValue('core', 'lastupdatedat', '0'); return new DataResponse(['status' => 'success', 'data' => ['message' => $this->l10n->t('Channel updated')]]); } @@ -99,7 +99,7 @@ class AdminController extends Controller { // Create a new job and store the creation date $this->jobList->add(ResetTokenBackgroundJob::class); - $this->config->setAppValue('core', 'updater.secret.created', $this->timeFactory->getTime()); + $this->config->setAppValue('core', 'updater.secret.created', (string)$this->timeFactory->getTime()); // Create a new token $newToken = $this->secureRandom->generate(64); diff --git a/apps/updatenotification/lib/Notification/BackgroundJob.php b/apps/updatenotification/lib/Notification/BackgroundJob.php index c1b3cddf945..f8f1f41e589 100644 --- a/apps/updatenotification/lib/Notification/BackgroundJob.php +++ b/apps/updatenotification/lib/Notification/BackgroundJob.php @@ -108,14 +108,14 @@ class BackgroundJob extends TimedJob { $status = $updater->check(); if ($status === false) { - $errors = 1 + (int) $this->config->getAppValue('updatenotification', 'update_check_errors', 0); - $this->config->setAppValue('updatenotification', 'update_check_errors', $errors); + $errors = 1 + (int) $this->config->getAppValue('updatenotification', 'update_check_errors', '0'); + $this->config->setAppValue('updatenotification', 'update_check_errors', (string)$errors); if (\in_array($errors, $this->connectionNotifications, true)) { $this->sendErrorNotifications($errors); } } elseif (\is_array($status)) { - $this->config->setAppValue('updatenotification', 'update_check_errors', 0); + $this->config->setAppValue('updatenotification', 'update_check_errors', '0'); $this->clearErrorNotifications(); if (isset($status['version'])) { diff --git a/apps/updatenotification/lib/Notification/Notifier.php b/apps/updatenotification/lib/Notification/Notifier.php index c1269daaa30..9ce233ed721 100644 --- a/apps/updatenotification/lib/Notification/Notifier.php +++ b/apps/updatenotification/lib/Notification/Notifier.php @@ -115,7 +115,7 @@ class Notifier implements INotifier { $l = $this->l10NFactory->get('updatenotification', $languageCode); if ($notification->getSubject() === 'connection_error') { - $errors = (int) $this->config->getAppValue('updatenotification', 'update_check_errors', 0); + $errors = (int) $this->config->getAppValue('updatenotification', 'update_check_errors', '0'); if ($errors === 0) { $this->notificationManager->markProcessed($notification); throw new \InvalidArgumentException('Update checked worked again'); diff --git a/apps/updatenotification/lib/Settings/Admin.php b/apps/updatenotification/lib/Settings/Admin.php index fded4451408..7f47a46f8f9 100644 --- a/apps/updatenotification/lib/Settings/Admin.php +++ b/apps/updatenotification/lib/Settings/Admin.php @@ -78,7 +78,7 @@ class Admin implements ISettings { } public function getForm(): TemplateResponse { - $lastUpdateCheckTimestamp = $this->config->getAppValue('core', 'lastupdatedat'); + $lastUpdateCheckTimestamp = (int)$this->config->getAppValue('core', 'lastupdatedat'); $lastUpdateCheck = $this->dateTimeFormatter->formatDateTime($lastUpdateCheckTimestamp); $channels = [ diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index 1cc0c62ff1d..d3510e7a398 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -589,6 +589,15 @@ class Access extends LDAPUtility { $altName = $this->createAltInternalOwnCloudName($intName, $isUser); if (is_string($altName)) { if ($this->mapAndAnnounceIfApplicable($mapper, $fdn, $altName, $uuid, $isUser)) { + $this->logger->warning( + 'Mapped {fdn} as {altName} because of a name collision on {intName}.', + [ + 'fdn' => $fdn, + 'altName' => $altName, + 'intName' => $intName, + 'app' => 'user_ldap', + ] + ); $newlyMapped = true; return $altName; } diff --git a/apps/user_ldap/lib/Jobs/Sync.php b/apps/user_ldap/lib/Jobs/Sync.php index b231089b79b..1ba24af5399 100644 --- a/apps/user_ldap/lib/Jobs/Sync.php +++ b/apps/user_ldap/lib/Jobs/Sync.php @@ -73,10 +73,10 @@ class Sync extends TimedJob { parent::__construct($time); $this->userManager = $userManager; $this->setInterval( - \OC::$server->getConfig()->getAppValue( + (int)\OC::$server->getConfig()->getAppValue( 'user_ldap', 'background_sync_interval', - self::MIN_INTERVAL + (string)self::MIN_INTERVAL ) ); } @@ -97,7 +97,7 @@ class Sync extends TimedJob { $interval = floor(24 * 60 * 60 / $runsPerDay); $interval = min(max($interval, self::MIN_INTERVAL), self::MAX_INTERVAL); - $this->config->setAppValue('user_ldap', 'background_sync_interval', $interval); + $this->config->setAppValue('user_ldap', 'background_sync_interval', (string)$interval); } /** @@ -198,7 +198,7 @@ class Sync extends TimedJob { $cycleData = [ 'prefix' => $this->config->getAppValue('user_ldap', 'background_sync_prefix', null), - 'offset' => (int)$this->config->getAppValue('user_ldap', 'background_sync_offset', 0), + 'offset' => (int)$this->config->getAppValue('user_ldap', 'background_sync_offset', '0'), ]; if ( @@ -255,7 +255,7 @@ class Sync extends TimedJob { * @return bool */ public function qualifiesToRun($cycleData) { - $lastChange = $this->config->getAppValue('user_ldap', $cycleData['prefix'] . '_lastChange', 0); + $lastChange = (int)$this->config->getAppValue('user_ldap', $cycleData['prefix'] . '_lastChange', '0'); if ((time() - $lastChange) > 60 * 30) { return true; } diff --git a/apps/user_ldap/lib/User/User.php b/apps/user_ldap/lib/User/User.php index 15894ce04b7..edf43494777 100644 --- a/apps/user_ldap/lib/User/User.php +++ b/apps/user_ldap/lib/User/User.php @@ -303,7 +303,7 @@ class User { } if (!is_null($attr) - && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', true) + && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', 'true') ) { // a naming rule attribute is defined, but it doesn't exist for that LDAP user throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername()); @@ -355,7 +355,7 @@ class User { */ public function markLogin() { $this->config->setUserValue( - $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, 1); + $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, '1'); } /** |