diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 90b5f1d4a2ce6..d7511e772abbf 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -79,6 +79,7 @@ 'OCA\\Files\\Listener\\NodeRemovedFromFavoriteListener' => $baseDir . '/../lib/Listener/NodeRemovedFromFavoriteListener.php', 'OCA\\Files\\Listener\\RenderReferenceEventListener' => $baseDir . '/../lib/Listener/RenderReferenceEventListener.php', 'OCA\\Files\\Listener\\SyncLivePhotosListener' => $baseDir . '/../lib/Listener/SyncLivePhotosListener.php', + 'OCA\\Files\\Listener\\UserFirstTimeLoggedInListener' => $baseDir . '/../lib/Listener/UserFirstTimeLoggedInListener.php', 'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php', 'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php', 'OCA\\Files\\Migration\\Version2003Date20241021095629' => $baseDir . '/../lib/Migration/Version2003Date20241021095629.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index 34c41adb82a25..ab2b19e7772c1 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -94,6 +94,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Listener\\NodeRemovedFromFavoriteListener' => __DIR__ . '/..' . '/../lib/Listener/NodeRemovedFromFavoriteListener.php', 'OCA\\Files\\Listener\\RenderReferenceEventListener' => __DIR__ . '/..' . '/../lib/Listener/RenderReferenceEventListener.php', 'OCA\\Files\\Listener\\SyncLivePhotosListener' => __DIR__ . '/..' . '/../lib/Listener/SyncLivePhotosListener.php', + 'OCA\\Files\\Listener\\UserFirstTimeLoggedInListener' => __DIR__ . '/..' . '/../lib/Listener/UserFirstTimeLoggedInListener.php', 'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php', 'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php', 'OCA\\Files\\Migration\\Version2003Date20241021095629' => __DIR__ . '/..' . '/../lib/Migration/Version2003Date20241021095629.php', diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 34883bb5be379..073e60ca2d167 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -25,6 +25,7 @@ use OCA\Files\Listener\NodeRemovedFromFavoriteListener; use OCA\Files\Listener\RenderReferenceEventListener; use OCA\Files\Listener\SyncLivePhotosListener; +use OCA\Files\Listener\UserFirstTimeLoggedInListener; use OCA\Files\Notification\Notifier; use OCA\Files\Search\FilesSearchProvider; use OCA\Files\Service\TagService; @@ -53,6 +54,7 @@ use OCP\ITagManager; use OCP\IUserSession; use OCP\Share\IManager as IShareManager; +use OCP\User\Events\UserFirstTimeLoggedInEvent; use OCP\Util; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; @@ -121,6 +123,8 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(LoadSearchPlugins::class, LoadSearchPluginsListener::class); $context->registerEventListener(NodeAddedToFavorite::class, NodeAddedToFavoriteListener::class); $context->registerEventListener(NodeRemovedFromFavorite::class, NodeRemovedFromFavoriteListener::class); + $context->registerEventListener(UserFirstTimeLoggedInEvent::class, UserFirstTimeLoggedInListener::class); + $context->registerSearchProvider(FilesSearchProvider::class); $context->registerNotifierService(Notifier::class); diff --git a/apps/files/lib/Listener/UserFirstTimeLoggedInListener.php b/apps/files/lib/Listener/UserFirstTimeLoggedInListener.php new file mode 100644 index 0000000000000..e89780392bbdb --- /dev/null +++ b/apps/files/lib/Listener/UserFirstTimeLoggedInListener.php @@ -0,0 +1,44 @@ + + */ +class UserFirstTimeLoggedInListener implements IEventListener { + public function __construct( + private readonly TemplateManager $templateManager, + private readonly ISetupManager $setupManager, + ) { + } + + public function handle(Event $event): void { + if (!$event instanceof UserFirstTimeLoggedInEvent) { + return; + } + + $user = $event->getUser(); + $this->setupManager->setupForUser($user); + + try { + // copy skeleton + $this->templateManager->copySkeleton($user->getUID()); + } catch (NotPermittedException) { + // read only uses + } + } +} diff --git a/lib/base.php b/lib/base.php index a11334c50cfd7..f8e8e38c83dcc 100644 --- a/lib/base.php +++ b/lib/base.php @@ -46,6 +46,7 @@ class OC { /** * The installation path for Nextcloud on the server (e.g. /srv/http/nextcloud) + * @internal Use auto-loaded $serverRoot with DI instead. */ public static string $SERVERROOT = ''; /** diff --git a/lib/private/Files/Template/TemplateManager.php b/lib/private/Files/Template/TemplateManager.php index d965a41957ad4..f545fc94a776d 100644 --- a/lib/private/Files/Template/TemplateManager.php +++ b/lib/private/Files/Template/TemplateManager.php @@ -64,9 +64,11 @@ public function __construct( private readonly IFactory $l10nFactory, private readonly LoggerInterface $logger, private readonly IFilenameValidator $filenameValidator, + private readonly string $serverRoot, ) { $this->l10n = $l10nFactory->get('lib'); $this->userId = $userSession->getUser()?->getUID(); + } #[Override] @@ -320,8 +322,8 @@ public function initializeTemplateDirectory(?string $path = null, ?string $userI $this->userId = $userId; } - $defaultSkeletonDirectory = \OC::$SERVERROOT . '/core/skeleton'; - $defaultTemplateDirectory = \OC::$SERVERROOT . '/core/skeleton/Templates'; + $defaultSkeletonDirectory = $this->serverRoot . '/core/skeleton'; + $defaultTemplateDirectory = $this->serverRoot . '/core/skeleton/Templates'; $skeletonPath = $this->config->getSystemValueString('skeletondirectory', $defaultSkeletonDirectory); $skeletonTemplatePath = $this->config->getSystemValueString('templatedirectory', $defaultTemplateDirectory); $isDefaultSkeleton = $skeletonPath === $defaultSkeletonDirectory; @@ -371,7 +373,7 @@ public function initializeTemplateDirectory(?string $path = null, ?string $userI if (!$isDefaultTemplates && $folderIsEmpty) { $localizedSkeletonTemplatePath = $this->getLocalizedTemplatePath($skeletonTemplatePath, $userLang); if (!empty($localizedSkeletonTemplatePath) && file_exists($localizedSkeletonTemplatePath)) { - \OC_Util::copyr($localizedSkeletonTemplatePath, $folder); + $this->copyr($localizedSkeletonTemplatePath, $folder); $userFolder->getStorage()->getScanner()->scan($folder->getInternalPath(), Scanner::SCAN_RECURSIVE); $this->setTemplatePath($userTemplatePath); return $userTemplatePath; @@ -381,7 +383,7 @@ public function initializeTemplateDirectory(?string $path = null, ?string $userI if ($path !== null && $isDefaultSkeleton && $isDefaultTemplates && $folderIsEmpty) { $localizedSkeletonPath = $this->getLocalizedTemplatePath($skeletonPath . '/Templates', $userLang); if (!empty($localizedSkeletonPath) && file_exists($localizedSkeletonPath)) { - \OC_Util::copyr($localizedSkeletonPath, $folder); + $this->copyr($localizedSkeletonPath, $folder); $userFolder->getStorage()->getScanner()->scan($folder->getInternalPath(), Scanner::SCAN_RECURSIVE); $this->setTemplatePath($userTemplatePath); return $userTemplatePath; @@ -412,4 +414,80 @@ private function getLocalizedTemplatePath(string $skeletonTemplatePath, string $ return $localizedSkeletonTemplatePath; } + + /** + * Copies a local directory recursively by using streams + */ + private function copyr(string $source, Folder $target): void { + // Verify if folder exists + $dir = opendir($source); + if ($dir === false) { + $this->logger->error(sprintf('Could not opendir "%s"', $source), ['app' => 'core']); + return; + } + + // Copy the files + while (false !== ($file = readdir($dir))) { + if (!Filesystem::isIgnoredDir($file)) { + if (is_dir($source . '/' . $file)) { + $child = $target->newFolder($file); + $this->copyr($source . '/' . $file, $child); + } else { + $sourceStream = fopen($source . '/' . $file, 'r'); + if ($sourceStream === false) { + $this->logger->error(sprintf('Could not fopen "%s"', $source . '/' . $file), ['app' => 'core']); + closedir($dir); + return; + } + $target->newFile($file, $sourceStream); + } + } + } + closedir($dir); + } + + public function copySkeleton(string $userId): void { + $user = $this->userManager->get($userId); + if ($user === null) { + throw new \LogicException('Trying to initialize home dir for a non-existent user'); + } + + $userDirectory = $this->rootFolder->getUserFolder($userId); + + $plainSkeletonDirectory = $this->config->getSystemValueString('skeletondirectory', $this->serverRoot . '/core/skeleton'); + $userLang = $this->l10nFactory->findLanguage(); + $skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory); + + if (!file_exists($skeletonDirectory)) { + $dialectStart = strpos($userLang, '_'); + if ($dialectStart !== false) { + $skeletonDirectory = str_replace('{lang}', substr($userLang, 0, $dialectStart), $plainSkeletonDirectory); + } + if ($dialectStart === false || !file_exists($skeletonDirectory)) { + $skeletonDirectory = str_replace('{lang}', 'default', $plainSkeletonDirectory); + } + if (!file_exists($skeletonDirectory)) { + $skeletonDirectory = ''; + } + } + + $instanceId = $this->config->getSystemValue('instanceid', ''); + + if ($instanceId === null) { + throw new \RuntimeException('no instance id!'); + } + $appdata = 'appdata_' . $instanceId; + if ($userId === $appdata) { + throw new \RuntimeException('username is reserved name: ' . $appdata); + } + + if (!empty($skeletonDirectory)) { + $this->logger->debug('copying skeleton for ' . $userId . ' from ' . $skeletonDirectory . ' to ' . $userDirectory->getFullPath('/'), ['app' => 'files_skeleton']); + $this->copyr($skeletonDirectory, $userDirectory); + // update the file cache + $userDirectory->getStorage()->getScanner()->scan('', Scanner::SCAN_RECURSIVE); + + $this->initializeTemplateDirectory(null, $userId); + } + } } diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index 651d5e8f9c42b..b0ee9b8fd52fb 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -20,7 +20,6 @@ use OC\Http\CookieHelper; use OC\Security\CSRF\CsrfTokenManager; use OC_User; -use OC_Util; use OCA\DAV\Connector\Sabre\Auth; use OCP\AppFramework\Db\TTransactional; use OCP\AppFramework\Utility\ITimeFactory; @@ -28,7 +27,6 @@ use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\EventDispatcher\GenericEvent; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\NotPermittedException; use OCP\IConfig; use OCP\IDBConnection; use OCP\IRequest; @@ -525,21 +523,6 @@ protected function prepareUserLogin($firstTimeLogin, $refreshCsrfToken = true) { } if ($firstTimeLogin) { - //we need to pass the user name, which may differ from login name - $user = $this->getUser()->getUID(); - OC_Util::setupFS($user); - - // TODO: lock necessary? - //trigger creation of user home and /files folder - $userFolder = \OC::$server->getUserFolder($user); - - try { - // copy skeleton - \OC_Util::copySkeleton($user, $userFolder); - } catch (NotPermittedException $ex) { - // read only uses - } - // trigger any other initialization Server::get(IEventDispatcher::class)->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser())); Server::get(IEventDispatcher::class)->dispatchTyped(new UserFirstTimeLoggedInEvent($this->getUser())); diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php index b3b0f4e3e20ca..dafa0d4cc1345 100644 --- a/lib/private/legacy/OC_Util.php +++ b/lib/private/legacy/OC_Util.php @@ -7,9 +7,9 @@ */ use bantu\IniGetWrapper\IniGetWrapper; use OC\Authentication\TwoFactorAuth\Manager as TwoFactorAuthManager; -use OC\Files\Cache\Scanner; use OC\Files\Filesystem; use OC\Files\SetupManager; +use OC\Files\Template\TemplateManager; use OC\Setup; use OC\SystemConfig; use OCP\App\IAppManager; @@ -17,7 +17,6 @@ use OCP\Files\Folder; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; -use OCP\Files\Template\ITemplateManager; use OCP\HintException; use OCP\IConfig; use OCP\IGroupManager; @@ -27,7 +26,6 @@ use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; -use OCP\L10N\IFactory; use OCP\Security\ISecureRandom; use OCP\Server; use OCP\Share\IManager; @@ -116,49 +114,10 @@ public static function isDefaultExpireDateEnforced() { * @param Folder $userDirectory * @throws NotFoundException * @throws NotPermittedException - * @suppress PhanDeprecatedFunction + * @deprecated 34.0.0 Not needed anymore, triggered automatically when UserFirstTimeLoggedInEvent is triggered */ public static function copySkeleton($userId, Folder $userDirectory) { - /** @var LoggerInterface $logger */ - $logger = Server::get(LoggerInterface::class); - - $plainSkeletonDirectory = Server::get(IConfig::class)->getSystemValueString('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton'); - $userLang = Server::get(IFactory::class)->findLanguage(); - $skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory); - - if (!file_exists($skeletonDirectory)) { - $dialectStart = strpos($userLang, '_'); - if ($dialectStart !== false) { - $skeletonDirectory = str_replace('{lang}', substr($userLang, 0, $dialectStart), $plainSkeletonDirectory); - } - if ($dialectStart === false || !file_exists($skeletonDirectory)) { - $skeletonDirectory = str_replace('{lang}', 'default', $plainSkeletonDirectory); - } - if (!file_exists($skeletonDirectory)) { - $skeletonDirectory = ''; - } - } - - $instanceId = Server::get(IConfig::class)->getSystemValue('instanceid', ''); - - if ($instanceId === null) { - throw new \RuntimeException('no instance id!'); - } - $appdata = 'appdata_' . $instanceId; - if ($userId === $appdata) { - throw new \RuntimeException('username is reserved name: ' . $appdata); - } - - if (!empty($skeletonDirectory)) { - $logger->debug('copying skeleton for ' . $userId . ' from ' . $skeletonDirectory . ' to ' . $userDirectory->getFullPath('/'), ['app' => 'files_skeleton']); - self::copyr($skeletonDirectory, $userDirectory); - // update the file cache - $userDirectory->getStorage()->getScanner()->scan('', Scanner::SCAN_RECURSIVE); - - /** @var ITemplateManager $templateManager */ - $templateManager = Server::get(ITemplateManager::class); - $templateManager->initializeTemplateDirectory(null, $userId); - } + Server::get(TemplateManager::class)->copySkeleton($userId); } /** @@ -167,6 +126,7 @@ public static function copySkeleton($userId, Folder $userDirectory) { * @param string $source * @param Folder $target * @return void + * @deprecated 34.0.0 Unused, if you really need this functionality, open an issue on GitHub */ public static function copyr($source, Folder $target) { $logger = Server::get(LoggerInterface::class); diff --git a/psalm.xml b/psalm.xml index ee406c5bcbf22..b0be3a9faa7de 100644 --- a/psalm.xml +++ b/psalm.xml @@ -63,6 +63,7 @@ + diff --git a/tests/lib/Files/Template/TemplateManagerTest.php b/tests/lib/Files/Template/TemplateManagerTest.php index 431b032594c14..dfafd7be61cac 100644 --- a/tests/lib/Files/Template/TemplateManagerTest.php +++ b/tests/lib/Files/Template/TemplateManagerTest.php @@ -7,7 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace lib\Files\Template; +namespace Test\Files\Template; use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Bootstrap\RegistrationContext; @@ -22,19 +22,18 @@ use OCP\IDBConnection; use OCP\IL10N; use OCP\IPreview; -use OCP\IServerContainer; use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Container\ContainerInterface; use Psr\Log\NullLogger; use Test\TestCase; class TemplateManagerTest extends TestCase { - - private IRootFolder $rootFolder; - private Coordinator $bootstrapCoordinator; - + private IRootFolder&MockObject $rootFolder; + private Coordinator&MockObject $bootstrapCoordinator; private TemplateManager $templateManager; protected function setUp(): void { @@ -58,7 +57,7 @@ protected function setUp(): void { $logger, ); - $serverContainer = $this->createMock(IServerContainer::class); + $serverContainer = $this->createMock(ContainerInterface::class); $eventDispatcher = $this->createMock(IEventDispatcher::class); $this->bootstrapCoordinator = $this->createMock(Coordinator::class); $this->bootstrapCoordinator->method('getRegistrationContext') @@ -83,7 +82,8 @@ protected function setUp(): void { $config, $l10nFactory, $logger, - $filenameValidator + $filenameValidator, + \OC::$SERVERROOT, ); } @@ -102,7 +102,7 @@ public function testCreateFromTemplateShoudValidateFilename(): void { return $this->createMock(Folder::class); }); $userFolder->method('nodeExists') - ->willReturnCallback(function ($path) use ($filePath, $fileDirectory) { + ->willReturnCallback(function ($path) use ($filePath, $fileDirectory): bool { return $path === $fileDirectory; }); $this->rootFolder->method('getUserFolder')