diff options
Diffstat (limited to 'lib')
20 files changed, 616 insertions, 51 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 2ce5d448518..79035f2e42a 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -260,6 +260,7 @@ return array( 'OCP\\Files\\Events\\FileCacheUpdated' => $baseDir . '/lib/public/Files/Events/FileCacheUpdated.php', 'OCP\\Files\\Events\\FileScannedEvent' => $baseDir . '/lib/public/Files/Events/FileScannedEvent.php', 'OCP\\Files\\Events\\FolderScannedEvent' => $baseDir . '/lib/public/Files/Events/FolderScannedEvent.php', + 'OCP\\Files\\Events\\InvalidateMountCacheEvent' => $baseDir . '/lib/public/Files/Events/InvalidateMountCacheEvent.php', 'OCP\\Files\\Events\\NodeAddedToCache' => $baseDir . '/lib/public/Files/Events/NodeAddedToCache.php', 'OCP\\Files\\Events\\NodeRemovedFromCache' => $baseDir . '/lib/public/Files/Events/NodeRemovedFromCache.php', 'OCP\\Files\\Events\\Node\\AbstractNodeEvent' => $baseDir . '/lib/public/Files/Events/Node/AbstractNodeEvent.php', @@ -808,6 +809,7 @@ return array( 'OC\\Command\\FileAccess' => $baseDir . '/lib/private/Command/FileAccess.php', 'OC\\Command\\QueueBus' => $baseDir . '/lib/private/Command/QueueBus.php', 'OC\\Comments\\Comment' => $baseDir . '/lib/private/Comments/Comment.php', + 'OC\\Comments\\EmojiHelper' => $baseDir . '/lib/private/Comments/EmojiHelper.php', 'OC\\Comments\\Manager' => $baseDir . '/lib/private/Comments/Manager.php', 'OC\\Comments\\ManagerFactory' => $baseDir . '/lib/private/Comments/ManagerFactory.php', 'OC\\Config' => $baseDir . '/lib/private/Config.php', @@ -1140,6 +1142,7 @@ return array( 'OC\\Files\\Node\\HookConnector' => $baseDir . '/lib/private/Files/Node/HookConnector.php', 'OC\\Files\\Node\\LazyFolder' => $baseDir . '/lib/private/Files/Node/LazyFolder.php', 'OC\\Files\\Node\\LazyRoot' => $baseDir . '/lib/private/Files/Node/LazyRoot.php', + 'OC\\Files\\Node\\LazyUserFolder' => $baseDir . '/lib/private/Files/Node/LazyUserFolder.php', 'OC\\Files\\Node\\Node' => $baseDir . '/lib/private/Files/Node/Node.php', 'OC\\Files\\Node\\NonExistingFile' => $baseDir . '/lib/private/Files/Node/NonExistingFile.php', 'OC\\Files\\Node\\NonExistingFolder' => $baseDir . '/lib/private/Files/Node/NonExistingFolder.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 4c35c1d1f85..4a4c886a082 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -289,6 +289,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Files\\Events\\FileCacheUpdated' => __DIR__ . '/../../..' . '/lib/public/Files/Events/FileCacheUpdated.php', 'OCP\\Files\\Events\\FileScannedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/FileScannedEvent.php', 'OCP\\Files\\Events\\FolderScannedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/FolderScannedEvent.php', + 'OCP\\Files\\Events\\InvalidateMountCacheEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/InvalidateMountCacheEvent.php', 'OCP\\Files\\Events\\NodeAddedToCache' => __DIR__ . '/../../..' . '/lib/public/Files/Events/NodeAddedToCache.php', 'OCP\\Files\\Events\\NodeRemovedFromCache' => __DIR__ . '/../../..' . '/lib/public/Files/Events/NodeRemovedFromCache.php', 'OCP\\Files\\Events\\Node\\AbstractNodeEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/Node/AbstractNodeEvent.php', @@ -837,6 +838,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Command\\FileAccess' => __DIR__ . '/../../..' . '/lib/private/Command/FileAccess.php', 'OC\\Command\\QueueBus' => __DIR__ . '/../../..' . '/lib/private/Command/QueueBus.php', 'OC\\Comments\\Comment' => __DIR__ . '/../../..' . '/lib/private/Comments/Comment.php', + 'OC\\Comments\\EmojiHelper' => __DIR__ . '/../../..' . '/lib/private/Comments/EmojiHelper.php', 'OC\\Comments\\Manager' => __DIR__ . '/../../..' . '/lib/private/Comments/Manager.php', 'OC\\Comments\\ManagerFactory' => __DIR__ . '/../../..' . '/lib/private/Comments/ManagerFactory.php', 'OC\\Config' => __DIR__ . '/../../..' . '/lib/private/Config.php', @@ -1169,6 +1171,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Files\\Node\\HookConnector' => __DIR__ . '/../../..' . '/lib/private/Files/Node/HookConnector.php', 'OC\\Files\\Node\\LazyFolder' => __DIR__ . '/../../..' . '/lib/private/Files/Node/LazyFolder.php', 'OC\\Files\\Node\\LazyRoot' => __DIR__ . '/../../..' . '/lib/private/Files/Node/LazyRoot.php', + 'OC\\Files\\Node\\LazyUserFolder' => __DIR__ . '/../../..' . '/lib/private/Files/Node/LazyUserFolder.php', 'OC\\Files\\Node\\Node' => __DIR__ . '/../../..' . '/lib/private/Files/Node/Node.php', 'OC\\Files\\Node\\NonExistingFile' => __DIR__ . '/../../..' . '/lib/private/Files/Node/NonExistingFile.php', 'OC\\Files\\Node\\NonExistingFolder' => __DIR__ . '/../../..' . '/lib/private/Files/Node/NonExistingFolder.php', diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index e604ac715c2..0a145bfd7e6 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -44,7 +44,7 @@ interface IProvider { * @param string $uid * @param string $loginName * @param string|null $password - * @param string $name + * @param string $name Name will be trimmed to 120 chars when longer * @param int $type token type * @param int $remember whether the session token should be used for remember-me * @return IToken diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index ae0874733f8..f8a0fb11c52 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -49,7 +49,7 @@ class Manager implements IProvider { * @param string $uid * @param string $loginName * @param string|null $password - * @param string $name + * @param string $name Name will be trimmed to 120 chars when longer * @param int $type token type * @param int $remember whether the session token should be used for remember-me * @return IToken @@ -62,7 +62,7 @@ class Manager implements IProvider { int $type = IToken::TEMPORARY_TOKEN, int $remember = IToken::DO_NOT_REMEMBER): IToken { if (mb_strlen($name) > 128) { - throw new InvalidTokenException('The given name is too long'); + $name = mb_substr($name, 0, 120) . '…'; } try { diff --git a/lib/private/Comments/EmojiHelper.php b/lib/private/Comments/EmojiHelper.php new file mode 100644 index 00000000000..a75919edbc9 --- /dev/null +++ b/lib/private/Comments/EmojiHelper.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OC\Comments; + +use OCP\IDBConnection; + +/** + * Copied OCA\UserStatus\Service\EmojiService + * Needs to be unified later + */ +class EmojiHelper { + + /** @var IDBConnection */ + private $db; + + /** + * EmojiService constructor. + * + * @param IDBConnection $db + */ + public function __construct(IDBConnection $db) { + $this->db = $db; + } + + /** + * @return bool + */ + public function doesPlatformSupportEmoji(): bool { + return $this->db->supports4ByteText() && + \class_exists(\IntlBreakIterator::class); + } + + /** + * @param string $emoji + * @return bool + */ + public function isValidEmoji(string $emoji): bool { + $intlBreakIterator = \IntlBreakIterator::createCharacterInstance(); + $intlBreakIterator->setText($emoji); + + $characterCount = 0; + while ($intlBreakIterator->next() !== \IntlBreakIterator::DONE) { + $characterCount++; + } + + if ($characterCount !== 1) { + return false; + } + + $codePointIterator = \IntlBreakIterator::createCodePointInstance(); + $codePointIterator->setText($emoji); + + foreach ($codePointIterator->getPartsIterator() as $codePoint) { + $codePointType = \IntlChar::charType($codePoint); + + // If the current code-point is an emoji or a modifier (like a skin-tone) + // just continue and check the next character + if ($codePointType === \IntlChar::CHAR_CATEGORY_MODIFIER_SYMBOL || + $codePointType === \IntlChar::CHAR_CATEGORY_MODIFIER_LETTER || + $codePointType === \IntlChar::CHAR_CATEGORY_OTHER_SYMBOL || + $codePointType === \IntlChar::CHAR_CATEGORY_GENERAL_OTHER_TYPES) { + continue; + } + + // If it's neither a modifier nor an emoji, we only allow + // a zero-width-joiner or a variation selector 16 + $codePointValue = \IntlChar::ord($codePoint); + if ($codePointValue === 8205 || $codePointValue === 65039) { + continue; + } + + return false; + } + + return true; + } +} diff --git a/lib/private/Comments/Manager.php b/lib/private/Comments/Manager.php index 384187accf3..123e4f6988d 100644 --- a/lib/private/Comments/Manager.php +++ b/lib/private/Comments/Manager.php @@ -59,6 +59,9 @@ class Manager implements ICommentsManager { /** @var ITimeFactory */ protected $timeFactory; + /** @var EmojiHelper */ + protected $emojiHelper; + /** @var IInitialStateService */ protected $initialStateService; @@ -78,11 +81,13 @@ class Manager implements ICommentsManager { LoggerInterface $logger, IConfig $config, ITimeFactory $timeFactory, + EmojiHelper $emojiHelper, IInitialStateService $initialStateService) { $this->dbConn = $dbConn; $this->logger = $logger; $this->config = $config; $this->timeFactory = $timeFactory; + $this->emojiHelper = $emojiHelper; $this->initialStateService = $initialStateService; } @@ -148,8 +153,9 @@ class Manager implements ICommentsManager { throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving'); } - if ($comment->getVerb() === 'reaction' && mb_strlen($comment->getMessage()) > 2) { - throw new \UnexpectedValueException('Reactions cannot be longer than 2 chars (emoji with skin tone have two chars)'); + if ($comment->getVerb() === 'reaction' && !$this->emojiHelper->isValidEmoji($comment->getMessage())) { + // 4 characters: laptop + person + gender + skin color => "🧑🏽💻" is a single emoji from the picker + throw new \UnexpectedValueException('Reactions can only be a single emoji'); } if ($comment->getId() === '') { diff --git a/lib/private/Contacts/ContactsMenu/ContactsStore.php b/lib/private/Contacts/ContactsMenu/ContactsStore.php index 5b7a942a244..0ac388ce00a 100644 --- a/lib/private/Contacts/ContactsMenu/ContactsStore.php +++ b/lib/private/Contacts/ContactsMenu/ContactsStore.php @@ -107,7 +107,7 @@ class ContactsStore implements IContactsStore { } $allContacts = $this->contactsManager->search( - $filter ?: '', + $filter ?? '', [ 'FN', 'EMAIL' @@ -146,7 +146,7 @@ class ContactsStore implements IContactsStore { * * @param IUser $self * @param Entry[] $entries - * @param string $filter + * @param string|null $filter * @return Entry[] the filtered contacts */ private function filterContacts( diff --git a/lib/private/Contacts/ContactsMenu/Manager.php b/lib/private/Contacts/ContactsMenu/Manager.php index cea67735da5..73a5a475d85 100644 --- a/lib/private/Contacts/ContactsMenu/Manager.php +++ b/lib/private/Contacts/ContactsMenu/Manager.php @@ -59,14 +59,14 @@ class Manager { /** * @param IUser $user - * @param string $filter + * @param string|null $filter * @return array */ public function getEntries(IUser $user, $filter) { $maxAutocompleteResults = max(0, $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT)); $minSearchStringLength = $this->config->getSystemValueInt('sharing.minSearchStringLength', 0); $topEntries = []; - if (strlen($filter) >= $minSearchStringLength) { + if (strlen($filter ?? '') >= $minSearchStringLength) { $entries = $this->store->getContacts($user, $filter, $maxAutocompleteResults); $sortedEntries = $this->sortEntries($entries); diff --git a/lib/private/Files/Config/MountProviderCollection.php b/lib/private/Files/Config/MountProviderCollection.php index cd8a2a2e29f..2b0acf7d69d 100644 --- a/lib/private/Files/Config/MountProviderCollection.php +++ b/lib/private/Files/Config/MountProviderCollection.php @@ -75,16 +75,15 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { } /** - * Get all configured mount points for the user - * - * @param \OCP\IUser $user - * @return \OCP\Files\Mount\IMountPoint[] + * @param IUser $user + * @param IMountProvider[] $providers + * @return IMountPoint[] */ - public function getMountsForUser(IUser $user) { + private function getUserMountsForProviders(IUser $user, array $providers): array { $loader = $this->loader; $mounts = array_map(function (IMountProvider $provider) use ($user, $loader) { return $provider->getMountsForUser($user, $loader); - }, $this->providers); + }, $providers); $mounts = array_filter($mounts, function ($result) { return is_array($result); }); @@ -94,14 +93,31 @@ class MountProviderCollection implements IMountProviderCollection, Emitter { return $this->filterMounts($user, $mounts); } - public function addMountForUser(IUser $user, IMountManager $mountManager) { + public function getMountsForUser(IUser $user): array { + return $this->getUserMountsForProviders($user, $this->providers); + } + + public function getUserMountsForProviderClass(IUser $user, string $mountProviderClass): array { + $providers = array_filter( + $this->providers, + fn (IMountProvider $mountProvider) => (get_class($mountProvider) === $mountProviderClass) + ); + return $this->getUserMountsForProviders($user, $providers); + } + + public function addMountForUser(IUser $user, IMountManager $mountManager, callable $providerFilter = null) { // shared mount provider gets to go last since it needs to know existing files // to check for name collisions $firstMounts = []; - $firstProviders = array_filter($this->providers, function (IMountProvider $provider) { + if ($providerFilter) { + $providers = array_filter($this->providers, $providerFilter); + } else { + $providers = $this->providers; + } + $firstProviders = array_filter($providers, function (IMountProvider $provider) { return (get_class($provider) !== 'OCA\Files_Sharing\MountProvider'); }); - $lastProviders = array_filter($this->providers, function (IMountProvider $provider) { + $lastProviders = array_filter($providers, function (IMountProvider $provider) { return (get_class($provider) === 'OCA\Files_Sharing\MountProvider'); }); foreach ($firstProviders as $provider) { diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index dc2640361e7..9a5eddc4878 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -89,7 +89,7 @@ class UserMountCache implements IUserMountCache { $this->mountsForUsers = new CappedMemoryCache(); } - public function registerMounts(IUser $user, array $mounts) { + public function registerMounts(IUser $user, array $mounts, array $mountProviderClasses = null) { // filter out non-proper storages coming from unit tests $mounts = array_filter($mounts, function (IMountPoint $mount) { return $mount instanceof SharedMount || $mount->getStorage() && $mount->getStorage()->getCache(); @@ -110,6 +110,11 @@ class UserMountCache implements IUserMountCache { $newMounts = array_combine($newMountRootIds, $newMounts); $cachedMounts = $this->getMountsForUser($user); + if (is_array($mountProviderClasses)) { + $cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses) { + return in_array($mountInfo->getMountProvider(), $mountProviderClasses); + }); + } $cachedMountRootIds = array_map(function (ICachedMountInfo $mount) { return $mount->getRootId(); }, $cachedMounts); @@ -446,4 +451,39 @@ class UserMountCache implements IUserMountCache { $this->cacheInfoCache = new CappedMemoryCache(); $this->mountsForUsers = new CappedMemoryCache(); } + + public function getMountForPath(IUser $user, string $path): ICachedMountInfo { + $mounts = $this->getMountsForUser($user); + $mountPoints = array_map(function (ICachedMountInfo $mount) { + return $mount->getMountPoint(); + }, $mounts); + $mounts = array_combine($mountPoints, $mounts); + + $current = $path; + // walk up the directory tree until we find a path that has a mountpoint set + // the loop will return if a mountpoint is found or break if none are found + while (true) { + $mountPoint = $current . '/'; + if (isset($mounts[$mountPoint])) { + return $mounts[$mountPoint]; + } elseif ($current === '') { + break; + } + + $current = dirname($current); + if ($current === '.' || $current === '/') { + $current = ''; + } + } + + throw new NotFoundException("No cached mount for path " . $path); + } + + public function getMountsInPath(IUser $user, string $path): array { + $path = rtrim($path, '/') . '/'; + $mounts = $this->getMountsForUser($user); + return array_filter($mounts, function (ICachedMountInfo $mount) use ($path) { + return $mount->getMountPoint() !== $path && strpos($mount->getMountPoint(), $path) === 0; + }); + } } diff --git a/lib/private/Files/Filesystem.php b/lib/private/Files/Filesystem.php index 9db9252037f..20b44e2736a 100644 --- a/lib/private/Files/Filesystem.php +++ b/lib/private/Files/Filesystem.php @@ -46,6 +46,7 @@ use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorageFactory; use OCP\IUser; use OCP\IUserManager; +use OCP\IUserSession; class Filesystem { @@ -324,6 +325,18 @@ class Filesystem { if (self::$defaultInstance) { return false; } + self::initInternal($root); + + //load custom mount config + self::initMountPoints($user); + + return true; + } + + public static function initInternal($root) { + if (self::$defaultInstance) { + return false; + } self::getLoader(); self::$defaultInstance = new View($root); /** @var IEventDispatcher $eventDispatcher */ @@ -338,9 +351,6 @@ class Filesystem { self::$mounts = \OC::$server->getMountManager(); } - //load custom mount config - self::initMountPoints($user); - self::$loaded = true; return true; @@ -378,6 +388,15 @@ class Filesystem { * @return View */ public static function getView() { + if (!self::$defaultInstance) { + /** @var IUserSession $session */ + $session = \OC::$server->get(IUserSession::class); + $user = $session->getUser(); + if ($user) { + $userDir = '/' . $user->getUID() . '/files'; + self::initInternal($userDir); + } + } return self::$defaultInstance; } @@ -736,7 +755,7 @@ class Filesystem { * @return \OC\Files\FileInfo|false False if file does not exist */ public static function getFileInfo($path, $includeMountPoints = true) { - return self::$defaultInstance->getFileInfo($path, $includeMountPoints); + return self::getView()->getFileInfo($path, $includeMountPoints); } /** diff --git a/lib/private/Files/Mount/Manager.php b/lib/private/Files/Mount/Manager.php index 66832690363..ecd97760f17 100644 --- a/lib/private/Files/Mount/Manager.php +++ b/lib/private/Files/Mount/Manager.php @@ -125,7 +125,7 @@ class Manager implements IMountManager { * @return IMountPoint[] */ public function findIn(string $path): array { - $this->setupManager->setupForPath($path); + $this->setupManager->setupForPath($path, true); $path = $this->formatPath($path); if (isset($this->inPathCache[$path])) { diff --git a/lib/private/Files/Node/LazyFolder.php b/lib/private/Files/Node/LazyFolder.php index bfdaeeccff7..45451e5c53c 100644 --- a/lib/private/Files/Node/LazyFolder.php +++ b/lib/private/Files/Node/LazyFolder.php @@ -25,6 +25,8 @@ declare(strict_types=1); */ namespace OC\Files\Node; +use OCP\Constants; + /** * Class LazyFolder * @@ -40,13 +42,16 @@ class LazyFolder implements \OCP\Files\Folder { /** @var LazyFolder | null */ protected $folder = null; + protected array $data; + /** * LazyFolder constructor. * * @param \Closure $folderClosure */ - public function __construct(\Closure $folderClosure) { + public function __construct(\Closure $folderClosure, array $data = []) { $this->folderClosure = $folderClosure; + $this->data = $data; } /** @@ -181,6 +186,9 @@ class LazyFolder implements \OCP\Files\Folder { * @inheritDoc */ public function getPath() { + if (isset($this->data['path'])) { + return $this->data['path']; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -230,6 +238,9 @@ class LazyFolder implements \OCP\Files\Folder { * @inheritDoc */ public function getPermissions() { + if (isset($this->data['permissions'])) { + return $this->data['permissions']; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -237,6 +248,9 @@ class LazyFolder implements \OCP\Files\Folder { * @inheritDoc */ public function isReadable() { + if (isset($this->data['permissions'])) { + return ($this->data['permissions'] & Constants::PERMISSION_READ) == Constants::PERMISSION_READ; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -244,6 +258,9 @@ class LazyFolder implements \OCP\Files\Folder { * @inheritDoc */ public function isUpdateable() { + if (isset($this->data['permissions'])) { + return ($this->data['permissions'] & Constants::PERMISSION_UPDATE) == Constants::PERMISSION_UPDATE; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -251,6 +268,9 @@ class LazyFolder implements \OCP\Files\Folder { * @inheritDoc */ public function isDeletable() { + if (isset($this->data['permissions'])) { + return ($this->data['permissions'] & Constants::PERMISSION_DELETE) == Constants::PERMISSION_DELETE; + } return $this->__call(__FUNCTION__, func_get_args()); } @@ -258,6 +278,9 @@ class LazyFolder implements \OCP\Files\Folder { * @inheritDoc */ public function isShareable() { + if (isset($this->data['permissions'])) { + return ($this->data['permissions'] & Constants::PERMISSION_SHARE) == Constants::PERMISSION_SHARE; + } return $this->__call(__FUNCTION__, func_get_args()); } diff --git a/lib/private/Files/Node/LazyUserFolder.php b/lib/private/Files/Node/LazyUserFolder.php new file mode 100644 index 00000000000..4c9e89ce233 --- /dev/null +++ b/lib/private/Files/Node/LazyUserFolder.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Files\Node; + +use OCP\Constants; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\IUser; + +class LazyUserFolder extends LazyFolder { + private IRootFolder $rootFolder; + private IUser $user; + + public function __construct(IRootFolder $rootFolder, IUser $user) { + $this->rootFolder = $rootFolder; + $this->user = $user; + parent::__construct(function () use ($user) { + try { + return $this->rootFolder->get('/' . $user->getUID() . '/files'); + } catch (NotFoundException $e) { + if (!$this->rootFolder->nodeExists('/' . $user->getUID())) { + $this->rootFolder->newFolder('/' . $user->getUID()); + } + return $this->rootFolder->newFolder('/' . $user->getUID() . '/files'); + } + }, [ + 'path' => '/' . $user->getUID() . '/files', + 'permissions' => Constants::PERMISSION_ALL, + ]); + } + + public function get($path) { + return $this->rootFolder->get('/' . $this->user->getUID() . '/files/' . rtrim($path, '/')); + } +} diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php index 88ac4a31d34..53162737b6f 100644 --- a/lib/private/Files/Node/Root.php +++ b/lib/private/Files/Node/Root.php @@ -380,15 +380,20 @@ class Root extends Folder implements IRootFolder { $userId = $userObject->getUID(); if (!$this->userFolderCache->hasKey($userId)) { - \OC\Files\Filesystem::initMountPoints($userId); - - try { - $folder = $this->get('/' . $userId . '/files'); - } catch (NotFoundException $e) { - if (!$this->nodeExists('/' . $userId)) { - $this->newFolder('/' . $userId); + if ($this->mountManager->getSetupManager()->isSetupComplete($userObject)) { + try { + $folder = $this->get('/' . $userId . '/files'); + if (!$folder instanceof \OCP\Files\Folder) { + throw new \Exception("User folder for $userId exists as a file"); + } + } catch (NotFoundException $e) { + if (!$this->nodeExists('/' . $userId)) { + $this->newFolder('/' . $userId); + } + $folder = $this->newFolder('/' . $userId . '/files'); } - $folder = $this->newFolder('/' . $userId . '/files'); + } else { + $folder = new LazyUserFolder($this, $userObject); } $this->userFolderCache->set($userId, $folder); diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php index 9726fbef428..da50983da32 100644 --- a/lib/private/Files/SetupManager.php +++ b/lib/private/Files/SetupManager.php @@ -42,14 +42,23 @@ use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\IMountProvider; use OCP\Files\Config\IUserMountCache; +use OCP\Files\Events\InvalidateMountCacheEvent; use OCP\Files\Events\Node\FilesystemTornDownEvent; use OCP\Files\Mount\IMountManager; use OCP\Files\Mount\IMountPoint; +use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorage; +use OCP\Group\Events\UserAddedEvent; +use OCP\Group\Events\UserRemovedEvent; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IConfig; use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; use OCP\Lockdown\ILockdownManager; +use OCP\Share\Events\ShareCreatedEvent; +use Psr\Log\LoggerInterface; class SetupManager { private bool $rootSetup = false; @@ -57,11 +66,19 @@ class SetupManager { private MountProviderCollection $mountProviderCollection; private IMountManager $mountManager; private IUserManager $userManager; + // List of users for which at least one mount is setup private array $setupUsers = []; + // List of users for which all mounts are setup + private array $setupUsersComplete = []; + /** @var array<string, string[]> */ + private array $setupUserMountProviders = []; private IEventDispatcher $eventDispatcher; private IUserMountCache $userMountCache; private ILockdownManager $lockdownManager; private IUserSession $userSession; + private ICache $cache; + private LoggerInterface $logger; + private IConfig $config; private bool $listeningForProviders; public function __construct( @@ -72,7 +89,10 @@ class SetupManager { IEventDispatcher $eventDispatcher, IUserMountCache $userMountCache, ILockdownManager $lockdownManager, - IUserSession $userSession + IUserSession $userSession, + ICacheFactory $cacheFactory, + LoggerInterface $logger, + IConfig $config ) { $this->eventLogger = $eventLogger; $this->mountProviderCollection = $mountProviderCollection; @@ -81,8 +101,21 @@ class SetupManager { $this->eventDispatcher = $eventDispatcher; $this->userMountCache = $userMountCache; $this->lockdownManager = $lockdownManager; + $this->logger = $logger; $this->userSession = $userSession; + $this->cache = $cacheFactory->createDistributed('setupmanager::'); $this->listeningForProviders = false; + $this->config = $config; + + $this->setupListeners(); + } + + private function isSetupStarted(IUser $user): bool { + return in_array($user->getUID(), $this->setupUsers, true); + } + + public function isSetupComplete(IUser $user): bool { + return in_array($user->getUID(), $this->setupUsersComplete, true); } private function setupBuiltinWrappers() { @@ -159,15 +192,33 @@ class SetupManager { * Setup the full filesystem for the specified user */ public function setupForUser(IUser $user): void { - $this->setupRoot(); + if ($this->isSetupComplete($user)) { + return; + } + $this->setupUsersComplete[] = $user->getUID(); + + if (!isset($this->setupUserMountProviders[$user->getUID()])) { + $this->setupUserMountProviders[$user->getUID()] = []; + } + $this->setupForUserWith($user, function () use ($user) { + $this->mountProviderCollection->addMountForUser($user, $this->mountManager, function ( + IMountProvider $provider + ) use ($user) { + return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]); + }); + }); + $this->afterUserFullySetup($user); + } + + /** + * part of the user setup that is run only once per user + */ + private function oneTimeUserSetup(IUser $user) { if (in_array($user->getUID(), $this->setupUsers, true)) { return; } $this->setupUsers[] = $user->getUID(); - - $this->eventLogger->start('setup_fs', 'Setup filesystem'); - $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false); OC_Hook::emit('OC_Filesystem', 'preSetup', ['user' => $user->getUID()]); @@ -176,7 +227,7 @@ class SetupManager { $userDir = '/' . $user->getUID() . '/files'; - Filesystem::init($user, $userDir); + Filesystem::initInternal($userDir); if ($this->lockdownManager->canAccessFilesystem()) { // home mounts are handled separate since we need to ensure this is mounted before we call the other mount providers @@ -187,13 +238,6 @@ class SetupManager { $homeMount->getStorage()->mkdir(''); $homeMount->getStorage()->getScanner()->scan(''); } - - // Chance to mount for other storages - $mounts = $this->mountProviderCollection->addMountForUser($user, $this->mountManager); - $mounts[] = $homeMount; - $this->userMountCache->registerMounts($user, $mounts); - - $this->listenForNewMountProviders(); } else { $this->mountManager->addMount(new MountPoint( new NullStorage([]), @@ -203,9 +247,46 @@ class SetupManager { new NullStorage([]), '/' . $user->getUID() . '/files' )); + $this->setupUsersComplete[] = $user->getUID(); + } + + $this->listenForNewMountProviders(); + } + + /** + * Final housekeeping after a user has been fully setup + */ + private function afterUserFullySetup(IUser $user): void { + $userRoot = '/' . $user->getUID() . '/'; + $mounts = $this->mountManager->getAll(); + $mounts = array_filter($mounts, function (IMountPoint $mount) use ($userRoot) { + return strpos($mount->getMountPoint(), $userRoot) === 0; + }); + $this->userMountCache->registerMounts($user, $mounts); + } + + /** + * @param IUser $user + * @param IMountPoint $mounts + * @return void + * @throws \OCP\HintException + * @throws \OC\ServerNotAvailableException + */ + private function setupForUserWith(IUser $user, callable $mountCallback): void { + $this->setupRoot(); + + if (!$this->isSetupStarted($user)) { + $this->oneTimeUserSetup($user); + } + + $this->eventLogger->start('setup_fs', 'Setup filesystem'); + + if ($this->lockdownManager->canAccessFilesystem()) { + $mountCallback(); } \OC_Hook::emit('OC_Filesystem', 'post_initMountPoints', ['user' => $user->getUID()]); + $userDir = '/' . $user->getUID() . '/files'; OC_Hook::emit('OC_Filesystem', 'setup', ['user' => $user->getUID(), 'user_dir' => $userDir]); $this->eventLogger->end('setup_fs'); @@ -242,7 +323,7 @@ class SetupManager { /** * Set up the filesystem for the specified path */ - public function setupForPath(string $path): void { + public function setupForPath(string $path, bool $includeChildren = false): void { if (substr_count($path, '/') < 2) { if ($user = $this->userSession->getUser()) { $this->setupForUser($user); @@ -264,11 +345,81 @@ class SetupManager { return; } - $this->setupForUser($user); + if ($this->isSetupComplete($user)) { + return; + } + + // we perform a "cached" setup only after having done the full setup recently + // this is also used to trigger a full setup after handling events that are likely + // to change the available mounts + $cachedSetup = $this->cache->get($user->getUID()); + if (!$cachedSetup) { + $this->setupForUser($user); + + $cacheDuration = $this->config->getSystemValueInt('fs_mount_cache_duration', 5 * 60); + if ($cacheDuration > 0) { + $this->cache->set($user->getUID(), true, $cacheDuration); + } + return; + } + + if (!isset($this->setupUserMountProviders[$user->getUID()])) { + $this->setupUserMountProviders[$user->getUID()] = []; + } + $setupProviders = &$this->setupUserMountProviders[$user->getUID()]; + $currentProviders = []; + + try { + $cachedMount = $this->userMountCache->getMountForPath($user, $path); + } catch (NotFoundException $e) { + $this->setupForUser($user); + return; + } + + $mounts = []; + if (!in_array($cachedMount->getMountProvider(), $setupProviders)) { + $setupProviders[] = $cachedMount->getMountProvider(); + $currentProviders[] = $cachedMount->getMountProvider(); + if ($cachedMount->getMountProvider()) { + $mounts = $this->mountProviderCollection->getUserMountsForProviderClass($user, $cachedMount->getMountProvider()); + } else { + $this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup"); + $this->setupForUser($user); + return; + } + } + + if ($includeChildren) { + $subCachedMounts = $this->userMountCache->getMountsInPath($user, $path); + foreach ($subCachedMounts as $cachedMount) { + if (!in_array($cachedMount->getMountProvider(), $setupProviders)) { + $setupProviders[] = $cachedMount->getMountProvider(); + $currentProviders[] = $cachedMount->getMountProvider(); + if ($cachedMount->getMountProvider()) { + $mounts = array_merge($mounts, $this->mountProviderCollection->getUserMountsForProviderClass($user, $cachedMount->getMountProvider())); + } else { + $this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup"); + $this->setupForUser($user); + return; + } + } + } + } + + if (count($mounts)) { + $this->userMountCache->registerMounts($user, $mounts, $currentProviders); + $this->setupForUserWith($user, function () use ($mounts) { + array_walk($mounts, [$this->mountManager, 'addMount']); + }); + } elseif (!$this->isSetupStarted($user)) { + $this->oneTimeUserSetup($user); + } } public function tearDown() { $this->setupUsers = []; + $this->setupUsersComplete = []; + $this->setupUserMountProviders = []; $this->rootSetup = false; $this->mountManager->clear(); $this->eventDispatcher->dispatchTyped(new FilesystemTornDownEvent()); @@ -280,7 +431,9 @@ class SetupManager { private function listenForNewMountProviders() { if (!$this->listeningForProviders) { $this->listeningForProviders = true; - $this->mountProviderCollection->listen('\OC\Files\Config', 'registerMountProvider', function (IMountProvider $provider) { + $this->mountProviderCollection->listen('\OC\Files\Config', 'registerMountProvider', function ( + IMountProvider $provider + ) { foreach ($this->setupUsers as $userId) { $user = $this->userManager->get($userId); if ($user) { @@ -291,4 +444,40 @@ class SetupManager { }); } } + + private function setupListeners() { + // note that this event handling is intentionally pessimistic + // clearing the cache to often is better than not enough + + $this->eventDispatcher->addListener(UserAddedEvent::class, function (UserAddedEvent $event) { + $this->cache->remove($event->getUser()->getUID()); + }); + $this->eventDispatcher->addListener(UserRemovedEvent::class, function (UserRemovedEvent $event) { + $this->cache->remove($event->getUser()->getUID()); + }); + $this->eventDispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event) { + $this->cache->remove($event->getShare()->getSharedWith()); + }); + $this->eventDispatcher->addListener(InvalidateMountCacheEvent::class, function (InvalidateMountCacheEvent $event + ) { + if ($user = $event->getUser()) { + $this->cache->remove($user->getUID()); + } else { + $this->cache->clear(); + } + }); + + $genericEvents = [ + '\OCA\Circles::onCircleCreation', + '\OCA\Circles::onCircleDestruction', + '\OCA\Circles::onMemberNew', + '\OCA\Circles::onMemberLeaving', + ]; + + foreach ($genericEvents as $genericEvent) { + $this->eventDispatcher->addListener($genericEvent, function ($event) { + $this->cache->clear(); + }); + } + } } diff --git a/lib/private/Files/SetupManagerFactory.php b/lib/private/Files/SetupManagerFactory.php index 56e70d09961..1d9efbd411f 100644 --- a/lib/private/Files/SetupManagerFactory.php +++ b/lib/private/Files/SetupManagerFactory.php @@ -28,9 +28,12 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\IMountProviderCollection; use OCP\Files\Config\IUserMountCache; use OCP\Files\Mount\IMountManager; +use OCP\ICacheFactory; +use OCP\IConfig; use OCP\IUserManager; use OCP\IUserSession; use OCP\Lockdown\ILockdownManager; +use Psr\Log\LoggerInterface; class SetupManagerFactory { private IEventLogger $eventLogger; @@ -41,6 +44,9 @@ class SetupManagerFactory { private ILockdownManager $lockdownManager; private IUserSession $userSession; private ?SetupManager $setupManager; + private ICacheFactory $cacheFactory; + private LoggerInterface $logger; + private IConfig $config; public function __construct( IEventLogger $eventLogger, @@ -49,7 +55,10 @@ class SetupManagerFactory { IEventDispatcher $eventDispatcher, IUserMountCache $userMountCache, ILockdownManager $lockdownManager, - IUserSession $userSession + IUserSession $userSession, + ICacheFactory $cacheFactory, + LoggerInterface $logger, + IConfig $config ) { $this->eventLogger = $eventLogger; $this->mountProviderCollection = $mountProviderCollection; @@ -58,6 +67,9 @@ class SetupManagerFactory { $this->userMountCache = $userMountCache; $this->lockdownManager = $lockdownManager; $this->userSession = $userSession; + $this->cacheFactory = $cacheFactory; + $this->logger = $logger; + $this->config = $config; $this->setupManager = null; } @@ -72,6 +84,9 @@ class SetupManagerFactory { $this->userMountCache, $this->lockdownManager, $this->userSession, + $this->cacheFactory, + $this->logger, + $this->config ); } return $this->setupManager; diff --git a/lib/public/Files/Config/IMountProviderCollection.php b/lib/public/Files/Config/IMountProviderCollection.php index f845d72cee6..5894d06a388 100644 --- a/lib/public/Files/Config/IMountProviderCollection.php +++ b/lib/public/Files/Config/IMountProviderCollection.php @@ -39,6 +39,16 @@ interface IMountProviderCollection { public function getMountsForUser(IUser $user); /** + * Get the configured mount points for the user from a specific mount provider + * + * @param \OCP\IUser $user + * @param class-string<IMountProvider> $mountProviderClass + * @return \OCP\Files\Mount\IMountPoint[] + * @since 24.0.0 + */ + public function getUserMountsForProviderClass(IUser $user, string $mountProviderClass); + + /** * Get the configured home mount for this user * * @param \OCP\IUser $user diff --git a/lib/public/Files/Config/IUserMountCache.php b/lib/public/Files/Config/IUserMountCache.php index 08f95990d3c..4411200c7ae 100644 --- a/lib/public/Files/Config/IUserMountCache.php +++ b/lib/public/Files/Config/IUserMountCache.php @@ -25,6 +25,7 @@ namespace OCP\Files\Config; use OCP\Files\Mount\IMountPoint; +use OCP\Files\NotFoundException; use OCP\IUser; /** @@ -38,9 +39,10 @@ interface IUserMountCache { * * @param IUser $user * @param IMountPoint[] $mounts + * @param array|null $mountProviderClasses * @since 9.0.0 */ - public function registerMounts(IUser $user, array $mounts); + public function registerMounts(IUser $user, array $mounts, array $mountProviderClasses = null); /** * Get all cached mounts for a user @@ -125,4 +127,26 @@ interface IUserMountCache { * @since 20.0.0 */ public function clear(): void; + + /** + * Get all cached mounts for a user + * + * @param IUser $user + * @param string $path + * @return ICachedMountInfo + * @throws NotFoundException + * @since 24.0.0 + */ + public function getMountForPath(IUser $user, string $path): ICachedMountInfo; + + /** + * Get all cached mounts for a user inside a path + * + * @param IUser $user + * @param string $path + * @return ICachedMountInfo[] + * @throws NotFoundException + * @since 24.0.0 + */ + public function getMountsInPath(IUser $user, string $path): array; } diff --git a/lib/public/Files/Events/InvalidateMountCacheEvent.php b/lib/public/Files/Events/InvalidateMountCacheEvent.php new file mode 100644 index 00000000000..6508e168d4c --- /dev/null +++ b/lib/public/Files/Events/InvalidateMountCacheEvent.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Files\Events; + +use OCP\EventDispatcher\Event; +use OCP\IUser; + +/** + * Used to notify the filesystem setup manager that the available mounts for a user have changed + * + * @since 24.0.0 + */ +class InvalidateMountCacheEvent extends Event { + private ?IUser $user; + + /** + * @param IUser|null $user user + * + * @since 24.0.0 + */ + public function __construct(?IUser $user) { + parent::__construct(); + $this->user = $user; + } + + /** + * @return IUser|null user + * + * @since 24.0.0 + */ + public function getUser(): ?IUser { + return $this->user; + } +} |