diff options
Diffstat (limited to 'lib/private/Federation/CloudIdManager.php')
-rw-r--r-- | lib/private/Federation/CloudIdManager.php | 206 |
1 files changed, 150 insertions, 56 deletions
diff --git a/lib/private/Federation/CloudIdManager.php b/lib/private/Federation/CloudIdManager.php index 77bb9437ba2..c599d9046a6 100644 --- a/lib/private/Federation/CloudIdManager.php +++ b/lib/private/Federation/CloudIdManager.php @@ -3,51 +3,67 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2017, Robin Appelman <robin@icewind.nl> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Guillaume Virlet <github@virlet.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.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/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Federation; +use OCA\DAV\Events\CardUpdatedEvent; use OCP\Contacts\IManager; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Federation\ICloudId; use OCP\Federation\ICloudIdManager; +use OCP\Federation\ICloudIdResolver; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IURLGenerator; use OCP\IUserManager; +use OCP\User\Events\UserChangedEvent; class CloudIdManager implements ICloudIdManager { - /** @var IManager */ - private $contactsManager; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IUserManager */ - private $userManager; - - public function __construct(IManager $contactsManager, IURLGenerator $urlGenerator, IUserManager $userManager) { - $this->contactsManager = $contactsManager; - $this->urlGenerator = $urlGenerator; - $this->userManager = $userManager; + private ICache $memCache; + private ICache $displayNameCache; + private array $cache = []; + /** @var ICloudIdResolver[] */ + private array $cloudIdResolvers = []; + + public function __construct( + ICacheFactory $cacheFactory, + IEventDispatcher $eventDispatcher, + private IManager $contactsManager, + private IURLGenerator $urlGenerator, + private IUserManager $userManager, + ) { + $this->memCache = $cacheFactory->createDistributed('cloud_id_'); + $this->displayNameCache = $cacheFactory->createDistributed('cloudid_name_'); + $eventDispatcher->addListener(UserChangedEvent::class, [$this, 'handleUserEvent']); + $eventDispatcher->addListener(CardUpdatedEvent::class, [$this, 'handleCardEvent']); + } + + public function handleUserEvent(Event $event): void { + if ($event instanceof UserChangedEvent && $event->getFeature() === 'displayName') { + $userId = $event->getUser()->getUID(); + $key = $userId . '@local'; + unset($this->cache[$key]); + $this->memCache->remove($key); + } + } + + public function handleCardEvent(Event $event): void { + if ($event instanceof CardUpdatedEvent) { + $data = $event->getCardData()['carddata']; + foreach (explode("\r\n", $data) as $line) { + if (str_starts_with($line, 'CLOUD;')) { + $parts = explode(':', $line, 2); + if (isset($parts[1])) { + $key = $parts[1]; + unset($this->cache[$key]); + $this->memCache->remove($key); + } + } + } + } } /** @@ -58,12 +74,18 @@ class CloudIdManager implements ICloudIdManager { public function resolveCloudId(string $cloudId): ICloudId { // TODO magic here to get the url and user instead of just splitting on @ + foreach ($this->cloudIdResolvers as $resolver) { + if ($resolver->isValidCloudId($cloudId)) { + return $resolver->resolveCloudId($cloudId); + } + } + if (!$this->isValidCloudId($cloudId)) { throw new \InvalidArgumentException('Invalid cloud id'); } // Find the first character that is not allowed in user names - $id = $this->fixRemoteURL($cloudId); + $id = $this->stripShareLinkFragments($cloudId); $posSlash = strpos($id, '/'); $posColon = strpos($id, ':'); @@ -82,14 +104,29 @@ class CloudIdManager implements ICloudIdManager { if ($lastValidAtPos !== false) { $user = substr($id, 0, $lastValidAtPos); $remote = substr($id, $lastValidAtPos + 1); + + // We accept slightly more chars when working with federationId than with a local userId. + // We remove those eventual chars from the UserId before using + // the IUserManager API to confirm its format. + $this->userManager->validateUserId(str_replace('=', '-', $user)); + if (!empty($user) && !empty($remote)) { - return new CloudId($id, $user, $remote, $this->getDisplayNameFromContact($id)); + $remote = $this->ensureDefaultProtocol($remote); + return new CloudId($id, $user, $remote, null); } } throw new \InvalidArgumentException('Invalid cloud id'); } - protected function getDisplayNameFromContact(string $cloudId): ?string { + public function getDisplayNameFromContact(string $cloudId): ?string { + $cachedName = $this->displayNameCache->get($cloudId); + if ($cachedName !== null) { + if ($cachedName === $cloudId) { + return null; + } + return $cachedName; + } + $addressBookEntries = $this->contactsManager->search($cloudId, ['CLOUD'], [ 'limit' => 1, 'enumeration' => false, @@ -100,17 +137,20 @@ class CloudIdManager implements ICloudIdManager { if (isset($entry['CLOUD'])) { foreach ($entry['CLOUD'] as $cloudID) { if ($cloudID === $cloudId) { - // Warning, if user decides to make his full name local only, + // Warning, if user decides to make their full name local only, // no FN is found on federated servers if (isset($entry['FN'])) { + $this->displayNameCache->set($cloudId, $entry['FN'], 15 * 60); return $entry['FN']; } else { - return $cloudID; + $this->displayNameCache->set($cloudId, $cloudId, 15 * 60); + return null; } } } } } + $this->displayNameCache->set($cloudId, $cloudId, 15 * 60); return null; } @@ -120,35 +160,69 @@ class CloudIdManager implements ICloudIdManager { * @return CloudId */ public function getCloudId(string $user, ?string $remote): ICloudId { - if ($remote === null) { - $remote = rtrim($this->removeProtocolFromUrl($this->urlGenerator->getAbsoluteURL('/')), '/'); - $fixedRemote = $this->fixRemoteURL($remote); + $isLocal = $remote === null; + if ($isLocal) { + $remote = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/'); + } + + // note that for remote id's we don't strip the protocol for the remote we use to construct the CloudId + // this way if a user has an explicit non-https cloud id this will be preserved + // we do still use the version without protocol for looking up the display name + $remote = $this->stripShareLinkFragments($remote); + $host = $this->removeProtocolFromUrl($remote); + $remote = $this->ensureDefaultProtocol($remote); + + $key = $user . '@' . ($isLocal ? 'local' : $host); + $cached = $this->cache[$key] ?? $this->memCache->get($key); + if ($cached) { + $this->cache[$key] = $cached; // put items from memcache into local cache + return new CloudId($cached['id'], $cached['user'], $cached['remote'], $cached['displayName']); + } + + if ($isLocal) { $localUser = $this->userManager->get($user); - $displayName = !is_null($localUser) ? $localUser->getDisplayName() : ''; + $displayName = $localUser ? $localUser->getDisplayName() : ''; } else { - // TODO check what the correct url is for remote (asking the remote) - $fixedRemote = $this->fixRemoteURL($remote); - $host = $this->removeProtocolFromUrl($fixedRemote); - $displayName = $this->getDisplayNameFromContact($user . '@' . $host); + $displayName = null; } - $id = $user . '@' . $remote; - return new CloudId($id, $user, $fixedRemote, $displayName); + + // For the visible cloudID we only strip away https + $id = $user . '@' . $this->removeProtocolFromUrl($remote, true); + + $data = [ + 'id' => $id, + 'user' => $user, + 'remote' => $remote, + 'displayName' => $displayName, + ]; + $this->cache[$key] = $data; + $this->memCache->set($key, $data, 15 * 60); + return new CloudId($id, $user, $remote, $displayName); } /** * @param string $url * @return string */ - private function removeProtocolFromUrl($url) { - if (strpos($url, 'https://') === 0) { - return substr($url, strlen('https://')); - } elseif (strpos($url, 'http://') === 0) { - return substr($url, strlen('http://')); + public function removeProtocolFromUrl(string $url, bool $httpsOnly = false): string { + if (str_starts_with($url, 'https://')) { + return substr($url, 8); + } + if (!$httpsOnly && str_starts_with($url, 'http://')) { + return substr($url, 7); } return $url; } + protected function ensureDefaultProtocol(string $remote): string { + if (!str_contains($remote, '://')) { + $remote = 'https://' . $remote; + } + + return $remote; + } + /** * Strips away a potential file names and trailing slashes: * - http://localhost @@ -161,7 +235,7 @@ class CloudIdManager implements ICloudIdManager { * @param string $remote * @return string */ - protected function fixRemoteURL(string $remote): string { + protected function stripShareLinkFragments(string $remote): string { $remote = str_replace('\\', '/', $remote); if ($fileNamePosition = strpos($remote, '/index.php')) { $remote = substr($remote, 0, $fileNamePosition); @@ -176,6 +250,26 @@ class CloudIdManager implements ICloudIdManager { * @return bool */ public function isValidCloudId(string $cloudId): bool { + foreach ($this->cloudIdResolvers as $resolver) { + if ($resolver->isValidCloudId($cloudId)) { + return true; + } + } + return strpos($cloudId, '@') !== false; } + + public function createCloudId(string $id, string $user, string $remote, ?string $displayName = null): ICloudId { + return new CloudId($id, $user, $remote, $displayName); + } + + public function registerCloudIdResolver(ICloudIdResolver $resolver): void { + array_unshift($this->cloudIdResolvers, $resolver); + } + + public function unregisterCloudIdResolver(ICloudIdResolver $resolver): void { + if (($key = array_search($resolver, $this->cloudIdResolvers)) !== false) { + array_splice($this->cloudIdResolvers, $key, 1); + } + } } |