Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/files/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions apps/files/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
44 changes: 44 additions & 0 deletions apps/files/lib/Listener/UserFirstTimeLoggedInListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files\Listener;

use OC\Files\Template\TemplateManager;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\ISetupManager;
use OCP\Files\NotPermittedException;
use OCP\User\Events\UserFirstTimeLoggedInEvent;

/**
* @template-implements IEventListener<UserFirstTimeLoggedInEvent>
*/
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
}
}
}
1 change: 1 addition & 0 deletions lib/base.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
/**
Expand Down
86 changes: 82 additions & 4 deletions lib/private/Files/Template/TemplateManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
17 changes: 0 additions & 17 deletions lib/private/User/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,13 @@
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;
use OCP\Authentication\Exceptions\ExpiredTokenException;
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;
Expand Down Expand Up @@ -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()));
Expand Down
48 changes: 4 additions & 44 deletions lib/private/legacy/OC_Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@
*/
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;
use OCP\Files\FileInfo;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<file name="status.php"/>
<file name="version.php"/>
<file name="tests/lib/TestCase.php"/>
<file name="tests/lib/Files/Template/*.php"/>
<ignoreFiles>
<directory name="apps/**/tests"/>
<directory name="apps/**/composer"/>
Expand Down
Loading
Loading