Skip to content

Commit 2b79955

Browse files
committed
refactor: Move copy skeleton step to a file listener
Instead of having all user providers call OC_Util::copySkeleton Signed-off-by: Carl Schwan <carlschwan@kde.org>
1 parent aa904b2 commit 2b79955

10 files changed

Lines changed: 148 additions & 86 deletions

File tree

apps/files/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
'OCA\\Files\\Listener\\NodeRemovedFromFavoriteListener' => $baseDir . '/../lib/Listener/NodeRemovedFromFavoriteListener.php',
8080
'OCA\\Files\\Listener\\RenderReferenceEventListener' => $baseDir . '/../lib/Listener/RenderReferenceEventListener.php',
8181
'OCA\\Files\\Listener\\SyncLivePhotosListener' => $baseDir . '/../lib/Listener/SyncLivePhotosListener.php',
82+
'OCA\\Files\\Listener\\UserFirstTimeLoggedInListener' => $baseDir . '/../lib/Listener/UserFirstTimeLoggedInListener.php',
8283
'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
8384
'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php',
8485
'OCA\\Files\\Migration\\Version2003Date20241021095629' => $baseDir . '/../lib/Migration/Version2003Date20241021095629.php',

apps/files/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class ComposerStaticInitFiles
9494
'OCA\\Files\\Listener\\NodeRemovedFromFavoriteListener' => __DIR__ . '/..' . '/../lib/Listener/NodeRemovedFromFavoriteListener.php',
9595
'OCA\\Files\\Listener\\RenderReferenceEventListener' => __DIR__ . '/..' . '/../lib/Listener/RenderReferenceEventListener.php',
9696
'OCA\\Files\\Listener\\SyncLivePhotosListener' => __DIR__ . '/..' . '/../lib/Listener/SyncLivePhotosListener.php',
97+
'OCA\\Files\\Listener\\UserFirstTimeLoggedInListener' => __DIR__ . '/..' . '/../lib/Listener/UserFirstTimeLoggedInListener.php',
9798
'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
9899
'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php',
99100
'OCA\\Files\\Migration\\Version2003Date20241021095629' => __DIR__ . '/..' . '/../lib/Migration/Version2003Date20241021095629.php',

apps/files/lib/AppInfo/Application.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use OCA\Files\Listener\NodeRemovedFromFavoriteListener;
2626
use OCA\Files\Listener\RenderReferenceEventListener;
2727
use OCA\Files\Listener\SyncLivePhotosListener;
28+
use OCA\Files\Listener\UserFirstTimeLoggedInListener;
2829
use OCA\Files\Notification\Notifier;
2930
use OCA\Files\Search\FilesSearchProvider;
3031
use OCA\Files\Service\TagService;
@@ -53,6 +54,7 @@
5354
use OCP\ITagManager;
5455
use OCP\IUserSession;
5556
use OCP\Share\IManager as IShareManager;
57+
use OCP\User\Events\UserFirstTimeLoggedInEvent;
5658
use OCP\Util;
5759
use Psr\Container\ContainerInterface;
5860
use Psr\Log\LoggerInterface;
@@ -121,6 +123,8 @@ public function register(IRegistrationContext $context): void {
121123
$context->registerEventListener(LoadSearchPlugins::class, LoadSearchPluginsListener::class);
122124
$context->registerEventListener(NodeAddedToFavorite::class, NodeAddedToFavoriteListener::class);
123125
$context->registerEventListener(NodeRemovedFromFavorite::class, NodeRemovedFromFavoriteListener::class);
126+
$context->registerEventListener(UserFirstTimeLoggedInEvent::class, UserFirstTimeLoggedInListener::class);
127+
124128
$context->registerSearchProvider(FilesSearchProvider::class);
125129

126130
$context->registerNotifierService(Notifier::class);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Files\Listener;
11+
12+
use OC\Files\Template\TemplateManager;
13+
use OCP\EventDispatcher\Event;
14+
use OCP\EventDispatcher\IEventListener;
15+
use OCP\Files\ISetupManager;
16+
use OCP\Files\NotPermittedException;
17+
use OCP\User\Events\UserFirstTimeLoggedInEvent;
18+
19+
/**
20+
* @template-implements IEventListener<UserFirstTimeLoggedInEvent>
21+
*/
22+
class UserFirstTimeLoggedInListener implements IEventListener {
23+
public function __construct(
24+
private readonly TemplateManager $templateManager,
25+
private readonly ISetupManager $setupManager,
26+
) {
27+
}
28+
29+
public function handle(Event $event): void {
30+
if (!$event instanceof UserFirstTimeLoggedInEvent) {
31+
return;
32+
}
33+
34+
$user = $event->getUser();
35+
$this->setupManager->setupForUser($user);
36+
37+
try {
38+
// copy skeleton
39+
$this->templateManager->copySkeleton($user->getUID());
40+
} catch (NotPermittedException) {
41+
// read only uses
42+
}
43+
}
44+
}

lib/base.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
class OC {
4747
/**
4848
* The installation path for Nextcloud on the server (e.g. /srv/http/nextcloud)
49+
* @internal Use auto-loaded $serverRoot with DI instead.
4950
*/
5051
public static string $SERVERROOT = '';
5152
/**

lib/private/Files/Template/TemplateManager.php

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
use OCP\IL10N;
3434
use OCP\IPreview;
3535
use OCP\IUserManager;
36-
use OCP\IUserSession;
3736
use OCP\L10N\IFactory;
3837
use Override;
3938
use Psr\Container\ContainerInterface;
@@ -50,23 +49,22 @@ class TemplateManager implements ITemplateManager {
5049
/** @var array<class-string<ICustomTemplateProvider>, ICustomTemplateProvider>|null */
5150
private ?array $providers = null;
5251
private IL10n $l10n;
53-
private ?string $userId;
5452

5553
public function __construct(
5654
private readonly ContainerInterface $serverContainer,
5755
private readonly IEventDispatcher $eventDispatcher,
5856
private readonly Coordinator $bootstrapCoordinator,
5957
private readonly IRootFolder $rootFolder,
60-
IUserSession $userSession,
6158
private readonly IUserManager $userManager,
6259
private readonly IPreview $previewManager,
6360
private readonly IConfig $config,
6461
private readonly IFactory $l10nFactory,
6562
private readonly LoggerInterface $logger,
6663
private readonly IFilenameValidator $filenameValidator,
64+
private readonly string $serverRoot,
65+
private ?string $userId,
6766
) {
6867
$this->l10n = $l10nFactory->get('lib');
69-
$this->userId = $userSession->getUser()?->getUID();
7068
}
7169

7270
#[Override]
@@ -320,8 +318,8 @@ public function initializeTemplateDirectory(?string $path = null, ?string $userI
320318
$this->userId = $userId;
321319
}
322320

323-
$defaultSkeletonDirectory = \OC::$SERVERROOT . '/core/skeleton';
324-
$defaultTemplateDirectory = \OC::$SERVERROOT . '/core/skeleton/Templates';
321+
$defaultSkeletonDirectory = $this->serverRoot . '/core/skeleton';
322+
$defaultTemplateDirectory = $this->serverRoot . '/core/skeleton/Templates';
325323
$skeletonPath = $this->config->getSystemValueString('skeletondirectory', $defaultSkeletonDirectory);
326324
$skeletonTemplatePath = $this->config->getSystemValueString('templatedirectory', $defaultTemplateDirectory);
327325
$isDefaultSkeleton = $skeletonPath === $defaultSkeletonDirectory;
@@ -371,7 +369,7 @@ public function initializeTemplateDirectory(?string $path = null, ?string $userI
371369
if (!$isDefaultTemplates && $folderIsEmpty) {
372370
$localizedSkeletonTemplatePath = $this->getLocalizedTemplatePath($skeletonTemplatePath, $userLang);
373371
if (!empty($localizedSkeletonTemplatePath) && file_exists($localizedSkeletonTemplatePath)) {
374-
\OC_Util::copyr($localizedSkeletonTemplatePath, $folder);
372+
$this->copyr($localizedSkeletonTemplatePath, $folder);
375373
$userFolder->getStorage()->getScanner()->scan($folder->getInternalPath(), Scanner::SCAN_RECURSIVE);
376374
$this->setTemplatePath($userTemplatePath);
377375
return $userTemplatePath;
@@ -381,7 +379,7 @@ public function initializeTemplateDirectory(?string $path = null, ?string $userI
381379
if ($path !== null && $isDefaultSkeleton && $isDefaultTemplates && $folderIsEmpty) {
382380
$localizedSkeletonPath = $this->getLocalizedTemplatePath($skeletonPath . '/Templates', $userLang);
383381
if (!empty($localizedSkeletonPath) && file_exists($localizedSkeletonPath)) {
384-
\OC_Util::copyr($localizedSkeletonPath, $folder);
382+
$this->copyr($localizedSkeletonPath, $folder);
385383
$userFolder->getStorage()->getScanner()->scan($folder->getInternalPath(), Scanner::SCAN_RECURSIVE);
386384
$this->setTemplatePath($userTemplatePath);
387385
return $userTemplatePath;
@@ -412,4 +410,80 @@ private function getLocalizedTemplatePath(string $skeletonTemplatePath, string $
412410

413411
return $localizedSkeletonTemplatePath;
414412
}
413+
414+
/**
415+
* Copies a local directory recursively by using streams
416+
*/
417+
private function copyr(string $source, Folder $target): void {
418+
// Verify if folder exists
419+
$dir = opendir($source);
420+
if ($dir === false) {
421+
$this->logger->error(sprintf('Could not opendir "%s"', $source), ['app' => 'core']);
422+
return;
423+
}
424+
425+
// Copy the files
426+
while (false !== ($file = readdir($dir))) {
427+
if (!Filesystem::isIgnoredDir($file)) {
428+
if (is_dir($source . '/' . $file)) {
429+
$child = $target->newFolder($file);
430+
$this->copyr($source . '/' . $file, $child);
431+
} else {
432+
$sourceStream = fopen($source . '/' . $file, 'r');
433+
if ($sourceStream === false) {
434+
$this->logger->error(sprintf('Could not fopen "%s"', $source . '/' . $file), ['app' => 'core']);
435+
closedir($dir);
436+
return;
437+
}
438+
$target->newFile($file, $sourceStream);
439+
}
440+
}
441+
}
442+
closedir($dir);
443+
}
444+
445+
public function copySkeleton(string $userId): void {
446+
$user = $this->userManager->get($userId);
447+
if ($user === null) {
448+
throw new \LogicException('Trying to initialize home dir for a non-existent user');
449+
}
450+
451+
$userDirectory = $this->rootFolder->getUserFolder($userId);
452+
453+
$plainSkeletonDirectory = $this->config->getSystemValueString('skeletondirectory', $this->serverRoot . '/core/skeleton');
454+
$userLang = $this->l10nFactory->findLanguage();
455+
$skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory);
456+
457+
if (!file_exists($skeletonDirectory)) {
458+
$dialectStart = strpos($userLang, '_');
459+
if ($dialectStart !== false) {
460+
$skeletonDirectory = str_replace('{lang}', substr($userLang, 0, $dialectStart), $plainSkeletonDirectory);
461+
}
462+
if ($dialectStart === false || !file_exists($skeletonDirectory)) {
463+
$skeletonDirectory = str_replace('{lang}', 'default', $plainSkeletonDirectory);
464+
}
465+
if (!file_exists($skeletonDirectory)) {
466+
$skeletonDirectory = '';
467+
}
468+
}
469+
470+
$instanceId = $this->config->getSystemValue('instanceid', '');
471+
472+
if ($instanceId === null) {
473+
throw new \RuntimeException('no instance id!');
474+
}
475+
$appdata = 'appdata_' . $instanceId;
476+
if ($userId === $appdata) {
477+
throw new \RuntimeException('username is reserved name: ' . $appdata);
478+
}
479+
480+
if (!empty($skeletonDirectory)) {
481+
$this->logger->debug('copying skeleton for ' . $userId . ' from ' . $skeletonDirectory . ' to ' . $userDirectory->getFullPath('/'), ['app' => 'files_skeleton']);
482+
$this->copyr($skeletonDirectory, $userDirectory);
483+
// update the file cache
484+
$userDirectory->getStorage()->getScanner()->scan('', Scanner::SCAN_RECURSIVE);
485+
486+
$this->initializeTemplateDirectory(null, $userId);
487+
}
488+
}
415489
}

lib/private/User/Session.php

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,13 @@
2020
use OC\Http\CookieHelper;
2121
use OC\Security\CSRF\CsrfTokenManager;
2222
use OC_User;
23-
use OC_Util;
2423
use OCA\DAV\Connector\Sabre\Auth;
2524
use OCP\AppFramework\Db\TTransactional;
2625
use OCP\AppFramework\Utility\ITimeFactory;
2726
use OCP\Authentication\Exceptions\ExpiredTokenException;
2827
use OCP\Authentication\Exceptions\InvalidTokenException;
2928
use OCP\EventDispatcher\GenericEvent;
3029
use OCP\EventDispatcher\IEventDispatcher;
31-
use OCP\Files\NotPermittedException;
3230
use OCP\IConfig;
3331
use OCP\IDBConnection;
3432
use OCP\IRequest;
@@ -525,21 +523,6 @@ protected function prepareUserLogin($firstTimeLogin, $refreshCsrfToken = true) {
525523
}
526524

527525
if ($firstTimeLogin) {
528-
//we need to pass the user name, which may differ from login name
529-
$user = $this->getUser()->getUID();
530-
OC_Util::setupFS($user);
531-
532-
// TODO: lock necessary?
533-
//trigger creation of user home and /files folder
534-
$userFolder = \OC::$server->getUserFolder($user);
535-
536-
try {
537-
// copy skeleton
538-
\OC_Util::copySkeleton($user, $userFolder);
539-
} catch (NotPermittedException $ex) {
540-
// read only uses
541-
}
542-
543526
// trigger any other initialization
544527
Server::get(IEventDispatcher::class)->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser()));
545528
Server::get(IEventDispatcher::class)->dispatchTyped(new UserFirstTimeLoggedInEvent($this->getUser()));

lib/private/legacy/OC_Util.php

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@
77
*/
88
use bantu\IniGetWrapper\IniGetWrapper;
99
use OC\Authentication\TwoFactorAuth\Manager as TwoFactorAuthManager;
10-
use OC\Files\Cache\Scanner;
1110
use OC\Files\Filesystem;
1211
use OC\Files\SetupManager;
12+
use OC\Files\Template\TemplateManager;
1313
use OC\Setup;
1414
use OC\SystemConfig;
1515
use OCP\App\IAppManager;
1616
use OCP\Files\FileInfo;
1717
use OCP\Files\Folder;
1818
use OCP\Files\NotFoundException;
1919
use OCP\Files\NotPermittedException;
20-
use OCP\Files\Template\ITemplateManager;
2120
use OCP\HintException;
2221
use OCP\IConfig;
2322
use OCP\IGroupManager;
@@ -27,7 +26,6 @@
2726
use OCP\IUser;
2827
use OCP\IUserManager;
2928
use OCP\IUserSession;
30-
use OCP\L10N\IFactory;
3129
use OCP\Security\ISecureRandom;
3230
use OCP\Server;
3331
use OCP\Share\IManager;
@@ -116,49 +114,10 @@ public static function isDefaultExpireDateEnforced() {
116114
* @param Folder $userDirectory
117115
* @throws NotFoundException
118116
* @throws NotPermittedException
119-
* @suppress PhanDeprecatedFunction
117+
* @deprecated 34.0.0 Not needed anymore, triggered automatically when UserFirstTimeLoggedInEvent is triggered
120118
*/
121119
public static function copySkeleton($userId, Folder $userDirectory) {
122-
/** @var LoggerInterface $logger */
123-
$logger = Server::get(LoggerInterface::class);
124-
125-
$plainSkeletonDirectory = Server::get(IConfig::class)->getSystemValueString('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton');
126-
$userLang = Server::get(IFactory::class)->findLanguage();
127-
$skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory);
128-
129-
if (!file_exists($skeletonDirectory)) {
130-
$dialectStart = strpos($userLang, '_');
131-
if ($dialectStart !== false) {
132-
$skeletonDirectory = str_replace('{lang}', substr($userLang, 0, $dialectStart), $plainSkeletonDirectory);
133-
}
134-
if ($dialectStart === false || !file_exists($skeletonDirectory)) {
135-
$skeletonDirectory = str_replace('{lang}', 'default', $plainSkeletonDirectory);
136-
}
137-
if (!file_exists($skeletonDirectory)) {
138-
$skeletonDirectory = '';
139-
}
140-
}
141-
142-
$instanceId = Server::get(IConfig::class)->getSystemValue('instanceid', '');
143-
144-
if ($instanceId === null) {
145-
throw new \RuntimeException('no instance id!');
146-
}
147-
$appdata = 'appdata_' . $instanceId;
148-
if ($userId === $appdata) {
149-
throw new \RuntimeException('username is reserved name: ' . $appdata);
150-
}
151-
152-
if (!empty($skeletonDirectory)) {
153-
$logger->debug('copying skeleton for ' . $userId . ' from ' . $skeletonDirectory . ' to ' . $userDirectory->getFullPath('/'), ['app' => 'files_skeleton']);
154-
self::copyr($skeletonDirectory, $userDirectory);
155-
// update the file cache
156-
$userDirectory->getStorage()->getScanner()->scan('', Scanner::SCAN_RECURSIVE);
157-
158-
/** @var ITemplateManager $templateManager */
159-
$templateManager = Server::get(ITemplateManager::class);
160-
$templateManager->initializeTemplateDirectory(null, $userId);
161-
}
120+
Server::get(TemplateManager::class)->copySkeleton($userId);
162121
}
163122

164123
/**
@@ -167,6 +126,7 @@ public static function copySkeleton($userId, Folder $userDirectory) {
167126
* @param string $source
168127
* @param Folder $target
169128
* @return void
129+
* @deprecated 34.0.0 Unused, if you really need this functionality, open an issue on GitHub
170130
*/
171131
public static function copyr($source, Folder $target) {
172132
$logger = Server::get(LoggerInterface::class);

psalm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
<file name="status.php"/>
6464
<file name="version.php"/>
6565
<file name="tests/lib/TestCase.php"/>
66+
<file name="tests/lib/Files/Template/*.php"/>
6667
<ignoreFiles>
6768
<directory name="apps/**/tests"/>
6869
<directory name="apps/**/composer"/>

0 commit comments

Comments
 (0)