Merge pull request #48560 from nextcloud/fix/migrate-encryption-away-from-hooks

feat(encryption): Migrate from hooks to events
This commit is contained in:
Côme Chilliet 2025-05-14 19:25:51 +02:00 committed by GitHub
commit 2cd491f491
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 285 additions and 371 deletions

View file

@ -10,7 +10,6 @@ namespace OCA\AdminAudit\Actions;
use OC\Files\Node\NonExistingFile;
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
use OCP\Files\Events\Node\BeforeNodeReadEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
use OCP\Files\Events\Node\NodeCopiedEvent;
use OCP\Files\Events\Node\NodeCreatedEvent;
use OCP\Files\Events\Node\NodeRenamedEvent;
@ -26,9 +25,6 @@ use Psr\Log\LoggerInterface;
* @package OCA\AdminAudit\Actions
*/
class Files extends Action {
private array $renamedNodes = [];
/**
* Logs file read actions
*/
@ -52,31 +48,16 @@ class Files extends Action {
);
}
/**
* Logs rename actions of files
*/
public function beforeRename(BeforeNodeRenamedEvent $event): void {
try {
$source = $event->getSource();
$this->renamedNodes[$source->getId()] = $source;
} catch (InvalidPathException|NotFoundException $e) {
Server::get(LoggerInterface::class)->error(
'Exception thrown in file rename: ' . $e->getMessage(), ['app' => 'admin_audit', 'exception' => $e]
);
return;
}
}
/**
* Logs rename actions of files
*/
public function afterRename(NodeRenamedEvent $event): void {
try {
$target = $event->getTarget();
$originalSource = $this->renamedNodes[$target->getId()];
$source = $event->getSource();
$params = [
'newid' => $target->getId(),
'oldpath' => $originalSource->getPath(),
'oldpath' => $source->getPath(),
'newpath' => $target->getPath(),
];
} catch (InvalidPathException|NotFoundException $e) {

View file

@ -42,7 +42,6 @@ use OCP\Console\ConsoleEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
use OCP\Files\Events\Node\BeforeNodeReadEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
use OCP\Files\Events\Node\NodeCopiedEvent;
use OCP\Files\Events\Node\NodeCreatedEvent;
use OCP\Files\Events\Node\NodeRenamedEvent;
@ -170,13 +169,6 @@ class Application extends App implements IBootstrap {
private function fileHooks(IAuditLogger $logger, IEventDispatcher $eventDispatcher): void {
$fileActions = new Files($logger);
$eventDispatcher->addListener(
BeforeNodeRenamedEvent::class,
function (BeforeNodeRenamedEvent $event) use ($fileActions): void {
$fileActions->beforeRename($event);
}
);
$eventDispatcher->addListener(
NodeRenamedEvent::class,
function (NodeRenamedEvent $event) use ($fileActions): void {

View file

@ -17,13 +17,22 @@ use Test\Traits\EncryptionTrait;
class EncryptedSizePropagationTest extends SizePropagationTest {
use EncryptionTrait;
protected function setUp(): void {
parent::setUp();
$this->config->setAppValue('encryption', 'useMasterKey', '0');
}
protected function setupUser($name, $password = '') {
$this->createUser($name, $password);
$tmpFolder = Server::get(ITempManager::class)->getTemporaryFolder();
$this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]);
$this->config->setAppValue('encryption', 'useMasterKey', '0');
$this->setupForUser($name, $password);
$this->loginWithEncryption($name);
return new View('/' . $name . '/files');
}
protected function loginHelper($user, $create = false, $password = false) {
$this->setupForUser($user, $password);
parent::loginHelper($user, $create, $password);
}
}

View file

@ -108,7 +108,7 @@ abstract class TestCase extends \Test\TestCase {
Server::get(DisplayNameCache::class)->clear();
//login as user1
self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
$this->loginHelper(self::TEST_FILES_SHARING_API_USER1);
$this->data = 'foobar';
$this->view = new View('/' . self::TEST_FILES_SHARING_API_USER1 . '/files');
@ -173,7 +173,7 @@ abstract class TestCase extends \Test\TestCase {
* @param bool $create
* @param bool $password
*/
protected static function loginHelper($user, $create = false, $password = false) {
protected function loginHelper($user, $create = false, $password = false) {
if ($password === false) {
$password = $user;
}

View file

@ -116,8 +116,11 @@ class TrashbinTest extends \Test\TestCase {
Server::get(IAppManager::class)->enableApp('files_trashbin');
$config = Server::get(IConfig::class);
$mockConfig = $this->createMock(IConfig::class);
$mockConfig
$mockConfig = $this->getMockBuilder(AllConfig::class)
->onlyMethods(['getSystemValue'])
->setConstructorArgs([Server::get(\OC\SystemConfig::class)])
->getMock();
$mockConfig->expects($this->any())
->method('getSystemValue')
->willReturnCallback(static function ($key, $default) use ($config) {
if ($key === 'filesystem_check_changes') {
@ -126,16 +129,6 @@ class TrashbinTest extends \Test\TestCase {
return $config->getSystemValue($key, $default);
}
});
$mockConfig
->method('getUserValue')
->willReturnCallback(static function ($userId, $appName, $key, $default = '') use ($config) {
return $config->getUserValue($userId, $appName, $key, $default);
});
$mockConfig
->method('getAppValue')
->willReturnCallback(static function ($appName, $key, $default = '') use ($config) {
return $config->getAppValue($appName, $key, $default);
});
$this->overwriteService(AllConfig::class, $mockConfig);
$this->trashRoot1 = '/' . self::TEST_TRASHBIN_USER1 . '/files_trashbin';

View file

@ -251,7 +251,7 @@ class FileEventsListener implements IEventListener {
/**
* Erase versions of deleted file
*
* This function is connected to the delete signal of OC_Filesystem
* This function is connected to the NodeDeletedEvent event
* cleanup the versions directory if the actual file gets deleted
*/
public function remove_hook(Node $node): void {
@ -283,7 +283,7 @@ class FileEventsListener implements IEventListener {
/**
* rename/move versions of renamed/moved files
*
* This function is connected to the rename signal of OC_Filesystem and adjust the name and location
* This function is connected to the NodeRenamedEvent event and adjust the name and location
* of the stored versions along the actual file
*/
public function rename_hook(Node $source, Node $target): void {
@ -302,7 +302,7 @@ class FileEventsListener implements IEventListener {
/**
* copy versions of copied files
*
* This function is connected to the copy signal of OC_Filesystem and copies the
* This function is connected to the NodeCopiedEvent event and copies the
* the stored versions to the new location
*/
public function copy_hook(Node $source, Node $target): void {

View file

@ -83,7 +83,10 @@ class VersioningTest extends \Test\TestCase {
parent::setUp();
$config = Server::get(IConfig::class);
$mockConfig = $this->createMock(IConfig::class);
$mockConfig = $this->getMockBuilder(AllConfig::class)
->onlyMethods(['getSystemValue'])
->setConstructorArgs([Server::get(\OC\SystemConfig::class)])
->getMock();
$mockConfig->expects($this->any())
->method('getSystemValue')
->willReturnCallback(function ($key, $default) use ($config) {
@ -427,8 +430,9 @@ class VersioningTest extends \Test\TestCase {
$this->rootView->file_put_contents($v2, 'version2');
// move file into the shared folder as recipient
Filesystem::rename('/test.txt', '/folder1/test.txt');
$success = Filesystem::rename('/test.txt', '/folder1/test.txt');
$this->assertTrue($success);
$this->assertFalse($this->rootView->file_exists($v1));
$this->assertFalse($this->rootView->file_exists($v2));

View file

@ -2013,9 +2013,6 @@
</UndefinedInterfaceMethod>
</file>
<file src="lib/private/Files/Storage/Wrapper/Encryption.php">
<InvalidOperand>
<code><![CDATA[$this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename)]]></code>
</InvalidOperand>
<InvalidReturnStatement>
<code><![CDATA[$result]]></code>
</InvalidReturnStatement>

View file

@ -6,13 +6,14 @@ declare(strict_types=1);
* SPDX-FileCopyrightText: 2013-2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
use OC\Encryption\HookManager;
use OC\Profiler\BuiltInProfiler;
use OC\Share20\GroupDeletedListener;
use OC\Share20\Hooks;
use OC\Share20\UserDeletedListener;
use OC\Share20\UserRemovedListener;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\BeforeFileSystemSetupEvent;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\Group\Events\UserRemovedEvent;
use OCP\IConfig;
@ -22,7 +23,6 @@ use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Server;
use OCP\Share;
use OCP\Template\ITemplateManager;
use OCP\User\Events\UserChangedEvent;
use OCP\User\Events\UserDeletedEvent;
@ -907,15 +907,16 @@ class OC {
}
private static function registerEncryptionWrapperAndHooks(): void {
/** @var \OC\Encryption\Manager */
$manager = Server::get(\OCP\Encryption\IManager::class);
\OCP\Util::connectHook('OC_Filesystem', 'preSetup', $manager, 'setupStorage');
Server::get(IEventDispatcher::class)->addListener(
BeforeFileSystemSetupEvent::class,
$manager->setupStorage(...),
);
$enabled = $manager->isEnabled();
if ($enabled) {
\OCP\Util::connectHook(Share::class, 'post_shared', HookManager::class, 'postShared');
\OCP\Util::connectHook(Share::class, 'post_unshare', HookManager::class, 'postUnshared');
\OCP\Util::connectHook('OC_Filesystem', 'post_rename', HookManager::class, 'postRename');
\OCP\Util::connectHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', HookManager::class, 'postRestore');
\OC\Encryption\EncryptionEventListener::register(Server::get(IEventDispatcher::class));
}
}

View file

@ -1539,6 +1539,7 @@ return array(
'OC\\DirectEditing\\Token' => $baseDir . '/lib/private/DirectEditing/Token.php',
'OC\\EmojiHelper' => $baseDir . '/lib/private/EmojiHelper.php',
'OC\\Encryption\\DecryptAll' => $baseDir . '/lib/private/Encryption/DecryptAll.php',
'OC\\Encryption\\EncryptionEventListener' => $baseDir . '/lib/private/Encryption/EncryptionEventListener.php',
'OC\\Encryption\\EncryptionWrapper' => $baseDir . '/lib/private/Encryption/EncryptionWrapper.php',
'OC\\Encryption\\Exceptions\\DecryptionFailedException' => $baseDir . '/lib/private/Encryption/Exceptions/DecryptionFailedException.php',
'OC\\Encryption\\Exceptions\\EmptyEncryptionDataException' => $baseDir . '/lib/private/Encryption/Exceptions/EmptyEncryptionDataException.php',
@ -1549,7 +1550,6 @@ return array(
'OC\\Encryption\\Exceptions\\ModuleDoesNotExistsException' => $baseDir . '/lib/private/Encryption/Exceptions/ModuleDoesNotExistsException.php',
'OC\\Encryption\\Exceptions\\UnknownCipherException' => $baseDir . '/lib/private/Encryption/Exceptions/UnknownCipherException.php',
'OC\\Encryption\\File' => $baseDir . '/lib/private/Encryption/File.php',
'OC\\Encryption\\HookManager' => $baseDir . '/lib/private/Encryption/HookManager.php',
'OC\\Encryption\\Keys\\Storage' => $baseDir . '/lib/private/Encryption/Keys/Storage.php',
'OC\\Encryption\\Manager' => $baseDir . '/lib/private/Encryption/Manager.php',
'OC\\Encryption\\Update' => $baseDir . '/lib/private/Encryption/Update.php',

View file

@ -1580,6 +1580,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\DirectEditing\\Token' => __DIR__ . '/../../..' . '/lib/private/DirectEditing/Token.php',
'OC\\EmojiHelper' => __DIR__ . '/../../..' . '/lib/private/EmojiHelper.php',
'OC\\Encryption\\DecryptAll' => __DIR__ . '/../../..' . '/lib/private/Encryption/DecryptAll.php',
'OC\\Encryption\\EncryptionEventListener' => __DIR__ . '/../../..' . '/lib/private/Encryption/EncryptionEventListener.php',
'OC\\Encryption\\EncryptionWrapper' => __DIR__ . '/../../..' . '/lib/private/Encryption/EncryptionWrapper.php',
'OC\\Encryption\\Exceptions\\DecryptionFailedException' => __DIR__ . '/../../..' . '/lib/private/Encryption/Exceptions/DecryptionFailedException.php',
'OC\\Encryption\\Exceptions\\EmptyEncryptionDataException' => __DIR__ . '/../../..' . '/lib/private/Encryption/Exceptions/EmptyEncryptionDataException.php',
@ -1590,7 +1591,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Encryption\\Exceptions\\ModuleDoesNotExistsException' => __DIR__ . '/../../..' . '/lib/private/Encryption/Exceptions/ModuleDoesNotExistsException.php',
'OC\\Encryption\\Exceptions\\UnknownCipherException' => __DIR__ . '/../../..' . '/lib/private/Encryption/Exceptions/UnknownCipherException.php',
'OC\\Encryption\\File' => __DIR__ . '/../../..' . '/lib/private/Encryption/File.php',
'OC\\Encryption\\HookManager' => __DIR__ . '/../../..' . '/lib/private/Encryption/HookManager.php',
'OC\\Encryption\\Keys\\Storage' => __DIR__ . '/../../..' . '/lib/private/Encryption/Keys/Storage.php',
'OC\\Encryption\\Manager' => __DIR__ . '/../../..' . '/lib/private/Encryption/Manager.php',
'OC\\Encryption\\Update' => __DIR__ . '/../../..' . '/lib/private/Encryption/Update.php',

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Encryption;
use OC\Files\SetupManager;
use OC\Files\View;
use OCA\Files_Trashbin\Events\NodeRestoredEvent;
use OCP\Encryption\IFile;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Events\Node\NodeRenamedEvent;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Share\Events\ShareCreatedEvent;
use OCP\Share\Events\ShareDeletedEvent;
use Psr\Log\LoggerInterface;
/** @template-implements IEventListener<NodeRenamedEvent|ShareCreatedEvent|ShareDeletedEvent|NodeRestoredEvent> */
class EncryptionEventListener implements IEventListener {
private ?Update $updater = null;
public function __construct(
private IUserSession $userSession,
private SetupManager $setupManager,
private Manager $encryptionManager,
) {
}
public static function register(IEventDispatcher $dispatcher): void {
$dispatcher->addServiceListener(NodeRenamedEvent::class, static::class);
$dispatcher->addServiceListener(ShareCreatedEvent::class, static::class);
$dispatcher->addServiceListener(ShareDeletedEvent::class, static::class);
$dispatcher->addServiceListener(NodeRestoredEvent::class, static::class);
}
public function handle(Event $event): void {
if (!$this->encryptionManager->isEnabled()) {
return;
}
if ($event instanceof NodeRenamedEvent) {
$this->getUpdate()->postRename($event->getSource(), $event->getTarget());
} elseif ($event instanceof ShareCreatedEvent) {
$this->getUpdate()->postShared($event->getShare()->getNode());
} elseif ($event instanceof ShareDeletedEvent) {
// In case the unsharing happens in a background job, we don't have
// a session and we load instead the user from the UserManager
$owner = $event->getShare()->getNode()->getOwner();
$this->getUpdate($owner)->postUnshared($event->getShare()->getNode());
} elseif ($event instanceof NodeRestoredEvent) {
$this->getUpdate()->postRestore($event->getTarget());
}
}
private function getUpdate(?IUser $owner = null): Update {
if (is_null($this->updater)) {
$user = $this->userSession->getUser();
if (!$user && ($owner !== null)) {
$user = $owner;
}
if (!$user) {
throw new \Exception('Inconsistent data, File unshared, but owner not found. Should not happen');
}
$uid = $user->getUID();
if (!$this->setupManager->isSetupComplete($user)) {
$this->setupManager->setupForUser($user);
}
$this->updater = new Update(
new Util(
new View(),
\OC::$server->getUserManager(),
\OC::$server->getGroupManager(),
\OC::$server->getConfig()),
\OC::$server->getEncryptionManager(),
\OC::$server->get(IFile::class),
\OC::$server->get(LoggerInterface::class),
$uid
);
}
return $this->updater;
}
}

View file

@ -75,15 +75,6 @@ class EncryptionWrapper {
\OC::$server->getGroupManager(),
\OC::$server->getConfig()
);
$update = new Update(
new View(),
$util,
Filesystem::getMountManager(),
$this->manager,
$fileHelper,
$this->logger,
$uid
);
return new Encryption(
$parameters,
$this->manager,
@ -92,7 +83,6 @@ class EncryptionWrapper {
$fileHelper,
$uid,
$keyStorage,
$update,
$mountManager,
$this->arrayCache
);

View file

@ -1,75 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Encryption;
use OC\Files\Filesystem;
use OC\Files\SetupManager;
use OC\Files\View;
use OCP\Encryption\IFile;
use Psr\Log\LoggerInterface;
class HookManager {
private static ?Update $updater = null;
public static function postShared($params): void {
self::getUpdate()->postShared($params);
}
public static function postUnshared($params): void {
// In case the unsharing happens in a background job, we don't have
// a session and we load instead the user from the UserManager
$path = Filesystem::getPath($params['fileSource']);
$owner = Filesystem::getOwner($path);
self::getUpdate($owner)->postUnshared($params);
}
public static function postRename($params): void {
self::getUpdate()->postRename($params);
}
public static function postRestore($params): void {
self::getUpdate()->postRestore($params);
}
private static function getUpdate(?string $owner = null): Update {
if (is_null(self::$updater)) {
$user = \OC::$server->getUserSession()->getUser();
if (!$user && $owner) {
$user = \OC::$server->getUserManager()->get($owner);
}
if (!$user) {
throw new \Exception('Inconsistent data, File unshared, but owner not found. Should not happen');
}
$uid = '';
if ($user) {
$uid = $user->getUID();
}
$setupManager = \OC::$server->get(SetupManager::class);
if (!$setupManager->isSetupComplete($user)) {
$setupManager->setupForUser($user);
}
self::$updater = new Update(
new View(),
new Util(
new View(),
\OC::$server->getUserManager(),
\OC::$server->getGroupManager(),
\OC::$server->getConfig()),
Filesystem::getMountManager(),
\OC::$server->getEncryptionManager(),
\OC::$server->get(IFile::class),
\OC::$server->get(LoggerInterface::class),
$uid
);
}
return self::$updater;
}
}

View file

@ -1,146 +1,85 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Encryption;
use InvalidArgumentException;
use OC\Files\Filesystem;
use OC\Files\Mount;
use OC\Files\View;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use OCP\Files\File as OCPFile;
use OCP\Files\Folder;
use OCP\Files\NotFoundException;
use Psr\Log\LoggerInterface;
/**
* update encrypted files, e.g. because a file was shared
*/
class Update {
/** @var View */
protected $view;
/** @var Util */
protected $util;
/** @var \OC\Files\Mount\Manager */
protected $mountManager;
/** @var Manager */
protected $encryptionManager;
/** @var string */
protected $uid;
/** @var File */
protected $file;
/** @var LoggerInterface */
protected $logger;
/**
* @param string $uid
*/
public function __construct(
View $view,
Util $util,
Mount\Manager $mountManager,
Manager $encryptionManager,
File $file,
LoggerInterface $logger,
$uid,
protected Util $util,
protected Manager $encryptionManager,
protected File $file,
protected LoggerInterface $logger,
protected string $uid,
) {
$this->view = $view;
$this->util = $util;
$this->mountManager = $mountManager;
$this->encryptionManager = $encryptionManager;
$this->file = $file;
$this->logger = $logger;
$this->uid = $uid;
}
/**
* hook after file was shared
*
* @param array $params
*/
public function postShared($params) {
if ($this->encryptionManager->isEnabled()) {
if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') {
$path = Filesystem::getPath($params['fileSource']);
[$owner, $ownerPath] = $this->getOwnerPath($path);
$absPath = '/' . $owner . '/files/' . $ownerPath;
$this->update($absPath);
}
}
public function postShared(OCPFile|Folder $node): void {
$this->update($node);
}
/**
* hook after file was unshared
*
* @param array $params
*/
public function postUnshared($params) {
if ($this->encryptionManager->isEnabled()) {
if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') {
$path = Filesystem::getPath($params['fileSource']);
[$owner, $ownerPath] = $this->getOwnerPath($path);
$absPath = '/' . $owner . '/files/' . $ownerPath;
$this->update($absPath);
}
}
public function postUnshared(OCPFile|Folder $node): void {
$this->update($node);
}
/**
* inform encryption module that a file was restored from the trash bin,
* e.g. to update the encryption keys
*
* @param array $params
*/
public function postRestore($params) {
if ($this->encryptionManager->isEnabled()) {
$path = Filesystem::normalizePath('/' . $this->uid . '/files/' . $params['filePath']);
$this->update($path);
}
public function postRestore(OCPFile|Folder $node): void {
$this->update($node);
}
/**
* inform encryption module that a file was renamed,
* e.g. to update the encryption keys
*
* @param array $params
*/
public function postRename($params) {
$source = $params['oldpath'];
$target = $params['newpath'];
if (
$this->encryptionManager->isEnabled() &&
dirname($source) !== dirname($target)
) {
[$owner, $ownerPath] = $this->getOwnerPath($target);
$absPath = '/' . $owner . '/files/' . $ownerPath;
$this->update($absPath);
public function postRename(OCPFile|Folder $source, OCPFile|Folder $target): void {
if (dirname($source->getPath()) !== dirname($target->getPath())) {
$this->update($target);
}
}
/**
* get owner and path relative to data/<owner>/files
* get owner and path relative to data/
*
* @param string $path path to file for current user
* @return array ['owner' => $owner, 'path' => $path]
* @throws \InvalidArgumentException
*/
protected function getOwnerPath($path) {
$info = Filesystem::getFileInfo($path);
$owner = Filesystem::getOwner($path);
$view = new View('/' . $owner . '/files');
$path = $view->getPath($info->getId());
if ($path === null) {
throw new InvalidArgumentException('No file found for ' . $info->getId());
protected function getOwnerPath(OCPFile|Folder $node): string {
$owner = $node->getOwner()?->getUID();
if ($owner === null) {
throw new InvalidArgumentException('No owner found for ' . $node->getId());
}
return [$owner, $path];
$view = new View('/' . $owner . '/files');
try {
$path = $view->getPath($node->getId());
} catch (NotFoundException $e) {
throw new InvalidArgumentException('No file found for ' . $node->getId(), previous:$e);
}
return '/' . $owner . '/files/' . $path;
}
/**
@ -149,7 +88,7 @@ class Update {
* @param string $path relative to data/
* @throws Exceptions\ModuleDoesNotExistsException
*/
public function update($path) {
public function update(OCPFile|Folder $node): void {
$encryptionModule = $this->encryptionManager->getEncryptionModule();
// if the encryption module doesn't encrypt the files on a per-user basis
@ -158,15 +97,14 @@ class Update {
return;
}
$path = $this->getOwnerPath($node);
// if a folder was shared, get a list of all (sub-)folders
if ($this->view->is_dir($path)) {
if ($node instanceof Folder) {
$allFiles = $this->util->getAllFiles($path);
} else {
$allFiles = [$path];
}
foreach ($allFiles as $file) {
$usersSharing = $this->file->getAccessList($file);
try {

View file

@ -304,7 +304,7 @@ class Util {
// detect user specific folders
if ($this->userManager->userExists($root[1])
&& in_array($root[2], $this->excludedPaths)) {
&& in_array($root[2] ?? '', $this->excludedPaths)) {
return true;
}
}

View file

@ -8,7 +8,6 @@
namespace OC\Files\Storage\Wrapper;
use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
use OC\Encryption\Update;
use OC\Encryption\Util;
use OC\Files\Cache\CacheEntry;
use OC\Files\Filesystem;
@ -50,7 +49,6 @@ class Encryption extends Wrapper {
private IFile $fileHelper,
private ?string $uid,
private IStorage $keyStorage,
private Update $update,
private Manager $mountManager,
private ArrayCache $arrayCache,
) {
@ -65,7 +63,8 @@ class Encryption extends Wrapper {
$info = $this->getCache()->get($path);
if ($info === false) {
return false;
/* Pass call to wrapped storage, it may be a special file like a part file */
return $this->storage->filesize($path);
}
if (isset($this->unencryptedSize[$fullPath])) {
$size = $this->unencryptedSize[$fullPath];
@ -319,7 +318,7 @@ class Encryption extends Wrapper {
if (!empty($encryptionModuleId)) {
$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
$shouldEncrypt = true;
} elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
} elseif ($info !== false && $info['encrypted'] === true) {
// we come from a old installation. No header and/or no module defined
// but the file is encrypted. In this case we need to use the
// OC_DEFAULT_MODULE to read the file
@ -537,10 +536,22 @@ class Encryption extends Wrapper {
$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
if ($result) {
if ($sourceStorage->is_dir($sourceInternalPath)) {
$result = $sourceStorage->rmdir($sourceInternalPath);
} else {
$result = $sourceStorage->unlink($sourceInternalPath);
$setPreserveCacheOnDelete = $sourceStorage->instanceOfStorage(ObjectStoreStorage::class) && !$this->instanceOfStorage(ObjectStoreStorage::class);
if ($setPreserveCacheOnDelete) {
/** @var ObjectStoreStorage $sourceStorage */
$sourceStorage->setPreserveCacheOnDelete(true);
}
try {
if ($sourceStorage->is_dir($sourceInternalPath)) {
$result = $sourceStorage->rmdir($sourceInternalPath);
} else {
$result = $sourceStorage->unlink($sourceInternalPath);
}
} finally {
if ($setPreserveCacheOnDelete) {
/** @var ObjectStoreStorage $sourceStorage */
$sourceStorage->setPreserveCacheOnDelete(false);
}
}
}
return $result;
@ -665,7 +676,7 @@ class Encryption extends Wrapper {
if (is_resource($dh)) {
while ($result && ($file = readdir($dh)) !== false) {
if (!Filesystem::isIgnoredDir($file)) {
$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
$result = $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, $preserveMtime, $isRename);
}
}
}

View file

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
@ -10,61 +13,67 @@ namespace Test\Encryption;
use OC\Encryption\File;
use OC\Encryption\Update;
use OC\Encryption\Util;
use OC\Files\Mount\Manager;
use OC\Files\View;
use OCP\Encryption\IEncryptionModule;
use OCP\Files\File as OCPFile;
use OCP\Files\Folder;
use OCP\IUser;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
class UpdateTest extends TestCase {
/** @var \OC\Encryption\Update */
private $update;
/** @var string */
private $uid;
/** @var \OC\Files\View | \PHPUnit\Framework\MockObject\MockObject */
private $view;
/** @var Util | \PHPUnit\Framework\MockObject\MockObject */
private $util;
/** @var \OC\Files\Mount\Manager | \PHPUnit\Framework\MockObject\MockObject */
private $mountManager;
/** @var \OC\Encryption\Manager | \PHPUnit\Framework\MockObject\MockObject */
private $encryptionManager;
/** @var \OCP\Encryption\IEncryptionModule | \PHPUnit\Framework\MockObject\MockObject */
private $encryptionModule;
/** @var \OC\Encryption\File | \PHPUnit\Framework\MockObject\MockObject */
private $fileHelper;
/** @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface */
private $logger;
private string $uid;
private View&MockObject $view;
private Util&MockObject $util;
private \OC\Encryption\Manager&MockObject $encryptionManager;
private IEncryptionModule&MockObject $encryptionModule;
private File&MockObject $fileHelper;
private LoggerInterface&MockObject $logger;
protected function setUp(): void {
parent::setUp();
$this->view = $this->createMock(View::class);
$this->util = $this->createMock(Util::class);
$this->mountManager = $this->createMock(Manager::class);
$this->encryptionManager = $this->createMock(\OC\Encryption\Manager::class);
$this->fileHelper = $this->createMock(File::class);
$this->encryptionModule = $this->createMock(IEncryptionModule::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->uid = 'testUser1';
}
$this->update = new Update(
$this->view,
$this->util,
$this->mountManager,
$this->encryptionManager,
$this->fileHelper,
$this->logger,
$this->uid);
private function getUserMock(string $uid): IUser&MockObject {
$user = $this->createMock(IUser::class);
$user->expects(self::any())
->method('getUID')
->willReturn($uid);
return $user;
}
private function getFileMock(string $path, string $owner): OCPFile&MockObject {
$node = $this->createMock(OCPFile::class);
$node->expects(self::atLeastOnce())
->method('getPath')
->willReturn($path);
$node->expects(self::any())
->method('getOwner')
->willReturn($this->getUserMock($owner));
return $node;
}
private function getFolderMock(string $path, string $owner): Folder&MockObject {
$node = $this->createMock(Folder::class);
$node->expects(self::atLeastOnce())
->method('getPath')
->willReturn($path);
$node->expects(self::any())
->method('getOwner')
->willReturn($this->getUserMock($owner));
return $node;
}
/**
@ -76,18 +85,21 @@ class UpdateTest extends TestCase {
* @param integer $numberOfFiles
*/
public function testUpdate($path, $isDir, $allFiles, $numberOfFiles): void {
$updateMock = $this->getUpdateMock(['getOwnerPath']);
$updateMock->expects($this->once())->method('getOwnerPath')
->willReturnCallback(fn (OCPFile|Folder $node) => '/user/' . $node->getPath());
$this->encryptionManager->expects($this->once())
->method('getEncryptionModule')
->willReturn($this->encryptionModule);
$this->view->expects($this->once())
->method('is_dir')
->willReturn($isDir);
if ($isDir) {
$this->util->expects($this->once())
->method('getAllFiles')
->willReturn($allFiles);
$node = $this->getFolderMock($path, 'user');
} else {
$node = $this->getFileMock($path, 'user');
}
$this->fileHelper->expects($this->exactly($numberOfFiles))
@ -98,7 +110,7 @@ class UpdateTest extends TestCase {
->method('update')
->willReturn(true);
$this->update->update($path);
$updateMock->update($node);
}
/**
@ -118,32 +130,26 @@ class UpdateTest extends TestCase {
*
* @param string $source
* @param string $target
* @param boolean $encryptionEnabled
*/
public function testPostRename($source, $target, $encryptionEnabled): void {
$updateMock = $this->getUpdateMock(['update', 'getOwnerPath']);
public function testPostRename($source, $target): void {
$updateMock = $this->getUpdateMock(['update','getOwnerPath']);
$this->encryptionManager->expects($this->once())
->method('isEnabled')
->willReturn($encryptionEnabled);
$sourceNode = $this->getFileMock($source, 'user');
$targetNode = $this->getFileMock($target, 'user');
if (dirname($source) === dirname($target) || $encryptionEnabled === false) {
if (dirname($source) === dirname($target)) {
$updateMock->expects($this->never())->method('getOwnerPath');
$updateMock->expects($this->never())->method('update');
} else {
$updateMock->expects($this->once())
->method('getOwnerPath')
->willReturnCallback(function ($path) use ($target) {
$this->assertSame(
$target,
$path,
'update needs to be executed for the target destination');
return ['owner', $path];
});
$updateMock->expects($this->once())->method('update');
$updateMock->expects($this->once())->method('update')
->willReturnCallback(fn (OCPFile|Folder $node) => $this->assertSame(
$target,
$node->getPath(),
'update needs to be executed for the target destination'
));
}
$updateMock->postRename(['oldpath' => $source, 'newpath' => $target]);
$updateMock->postRename($sourceNode, $targetNode);
}
/**
@ -153,62 +159,35 @@ class UpdateTest extends TestCase {
*/
public function dataTestPostRename() {
return [
['/test.txt', '/testNew.txt', true],
['/test.txt', '/testNew.txt', false],
['/folder/test.txt', '/testNew.txt', true],
['/folder/test.txt', '/testNew.txt', false],
['/folder/test.txt', '/testNew.txt', true],
['/test.txt', '/folder/testNew.txt', false],
['/test.txt', '/testNew.txt'],
['/folder/test.txt', '/testNew.txt'],
['/test.txt', '/folder/testNew.txt'],
];
}
/**
* @dataProvider dataTestPostRestore
*
* @param boolean $encryptionEnabled
*/
public function testPostRestore($encryptionEnabled): void {
public function testPostRestore(): void {
$updateMock = $this->getUpdateMock(['update']);
$this->encryptionManager->expects($this->once())
->method('isEnabled')
->willReturn($encryptionEnabled);
$updateMock->expects($this->once())->method('update')
->willReturnCallback(fn (OCPFile|Folder $node) => $this->assertSame(
'/folder/test.txt',
$node->getPath(),
'update needs to be executed for the target destination'
));
if ($encryptionEnabled) {
$updateMock->expects($this->once())->method('update');
} else {
$updateMock->expects($this->never())->method('update');
}
$updateMock->postRestore(['filePath' => '/folder/test.txt']);
}
/**
* test data for testPostRestore()
*
* @return array
*/
public function dataTestPostRestore() {
return [
[true],
[false],
];
$updateMock->postRestore($this->getFileMock('/folder/test.txt', 'user'));
}
/**
* create mock of the update method
*
* @param array $methods methods which should be set
* @return \OC\Encryption\Update | \PHPUnit\Framework\MockObject\MockObject
*/
protected function getUpdateMock($methods) {
return $this->getMockBuilder('\OC\Encryption\Update')
protected function getUpdateMock(array $methods): Update&MockObject {
return $this->getMockBuilder(Update::class)
->setConstructorArgs(
[
$this->view,
$this->util,
$this->mountManager,
$this->encryptionManager,
$this->fileHelper,
$this->logger,

View file

@ -11,7 +11,6 @@ use Exception;
use OC;
use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
use OC\Encryption\File;
use OC\Encryption\Update;
use OC\Encryption\Util;
use OC\Files\Cache\Cache;
use OC\Files\Cache\CacheEntry;
@ -46,7 +45,6 @@ class EncryptionTest extends Storage {
private Util&MockObject $util;
private \OC\Encryption\Manager&MockObject $encryptionManager;
private IEncryptionModule&MockObject $encryptionModule;
private Update&MockObject $update;
private Cache&MockObject $cache;
private LoggerInterface&MockObject $logger;
private File&MockObject $file;
@ -111,9 +109,6 @@ class EncryptionTest extends Storage {
$this->keyStore = $this->getMockBuilder('\OC\Encryption\Keys\Storage')
->disableOriginalConstructor()->getMock();
$this->update = $this->getMockBuilder('\OC\Encryption\Update')
->disableOriginalConstructor()->getMock();
$this->mount = $this->getMockBuilder('\OC\Files\Mount\MountPoint')
->disableOriginalConstructor()
->setMethods(['getOption'])
@ -155,7 +150,6 @@ class EncryptionTest extends Storage {
$this->file,
null,
$this->keyStore,
$this->update,
$this->mountManager,
$this->arrayCache
]
@ -237,7 +231,6 @@ class EncryptionTest extends Storage {
$this->file,
null,
$this->keyStore,
$this->update,
$this->mountManager,
$this->arrayCache,
]
@ -316,7 +309,6 @@ class EncryptionTest extends Storage {
$this->file,
null,
$this->keyStore,
$this->update,
$this->mountManager,
$this->arrayCache,
]
@ -361,7 +353,6 @@ class EncryptionTest extends Storage {
$this->file,
null,
$this->keyStore,
$this->update,
$this->mountManager,
$this->arrayCache,
]
@ -491,7 +482,6 @@ class EncryptionTest extends Storage {
$this->file,
null,
$this->keyStore,
$this->update,
$this->mountManager,
$this->arrayCache,
);
@ -598,7 +588,6 @@ class EncryptionTest extends Storage {
$this->file,
null,
$this->keyStore,
$this->update,
$this->mountManager,
$this->arrayCache,
]
@ -692,7 +681,6 @@ class EncryptionTest extends Storage {
$this->file,
null,
$this->keyStore,
$this->update,
$this->mountManager,
$this->arrayCache,
]
@ -867,7 +855,6 @@ class EncryptionTest extends Storage {
$this->file,
null,
$this->keyStore,
$this->update,
$this->mountManager,
$this->arrayCache
]
@ -968,7 +955,6 @@ class EncryptionTest extends Storage {
$util = $this->createMock(Util::class);
$fileHelper = $this->createMock(IFile::class);
$keyStorage = $this->createMock(IStorage::class);
$update = $this->createMock(Update::class);
$mountManager = $this->createMock(\OC\Files\Mount\Manager::class);
$mount = $this->createMock(IMountPoint::class);
$arrayCache = $this->createMock(ArrayCache::class);
@ -986,7 +972,6 @@ class EncryptionTest extends Storage {
$fileHelper,
null,
$keyStorage,
$update,
$mountManager,
$arrayCache
]

View file

@ -25,6 +25,7 @@ use OCP\Files\ForbiddenException;
use OCP\Files\GenericFileException;
use OCP\Files\Mount\IMountManager;
use OCP\Files\Storage\IStorage;
use OCP\Files\Storage\IStorageFactory;
use OCP\IDBConnection;
use OCP\IUserManager;
use OCP\Lock\ILockingProvider;
@ -518,10 +519,10 @@ class ViewTest extends \Test\TestCase {
}
public function moveBetweenStorages($storage1, $storage2) {
Filesystem::mount($storage1, [], '/');
Filesystem::mount($storage2, [], '/substorage');
Filesystem::mount($storage1, [], '/' . $this->user . '/');
Filesystem::mount($storage2, [], '/' . $this->user . '/substorage');
$rootView = new View('');
$rootView = new View('/' . $this->user);
$rootView->rename('foo.txt', 'substorage/folder/foo.txt');
$this->assertFalse($rootView->file_exists('foo.txt'));
$this->assertTrue($rootView->file_exists('substorage/folder/foo.txt'));
@ -941,14 +942,16 @@ class ViewTest extends \Test\TestCase {
$storage = new Temporary([]);
$scanner = $storage->getScanner();
Filesystem::mount($storage, [], '/test/');
$storage->file_put_contents('test.part', 'foobar');
$sizeWritten = $storage->file_put_contents('test.part', 'foobar');
$scanner->scan('');
$view = new View('/test');
$info = $view->getFileInfo('test.part');
$this->assertInstanceOf('\OCP\Files\FileInfo', $info);
$this->assertNull($info->getId());
$this->assertEquals(6, $sizeWritten);
$this->assertEquals(6, $info->getSize());
$this->assertEquals('foobar', $view->file_get_contents('test.part'));
}
public static function absolutePathProvider(): array {
@ -1947,6 +1950,8 @@ class ViewTest extends \Test\TestCase {
/* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
Server::get(ITrashManager::class)->pauseTrash();
/* Same thing with encryption wrapper */
Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
Filesystem::mount($storage, [], $this->user . '/');
@ -2097,6 +2102,8 @@ class ViewTest extends \Test\TestCase {
/* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
Server::get(ITrashManager::class)->pauseTrash();
/* Same thing with encryption wrapper */
Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
Filesystem::mount($storage, [], $this->user . '/');
@ -2238,6 +2245,9 @@ class ViewTest extends \Test\TestCase {
$sourcePath = 'original.txt';
$targetPath = 'target.txt';
/* Disable encryption wrapper to avoid it intercepting mocked call */
Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
Filesystem::mount($storage, [], $this->user . '/');
$storage->mkdir('files');
$view->file_put_contents($sourcePath, 'meh');
@ -2291,6 +2301,9 @@ class ViewTest extends \Test\TestCase {
$sourcePath = 'original.txt';
$targetPath = 'target.txt';
/* Disable encryption wrapper to avoid it intercepting mocked call */
Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
Filesystem::mount($storage, [], $this->user . '/');
$storage->mkdir('files');
$view->file_put_contents($sourcePath, 'meh');
@ -2427,6 +2440,9 @@ class ViewTest extends \Test\TestCase {
$sourcePath = 'original.txt';
$targetPath = 'substorage/target.txt';
/* Disable encryption wrapper to avoid it intercepting mocked call */
Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
Filesystem::mount($storage, [], $this->user . '/');
Filesystem::mount($storage2, [], $this->user . '/files/substorage');
$storage->mkdir('files');