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')