CloudIdManager.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Federation;
  8. use OCA\DAV\Events\CardUpdatedEvent;
  9. use OCP\Contacts\IManager;
  10. use OCP\EventDispatcher\Event;
  11. use OCP\EventDispatcher\IEventDispatcher;
  12. use OCP\Federation\ICloudId;
  13. use OCP\Federation\ICloudIdManager;
  14. use OCP\ICache;
  15. use OCP\ICacheFactory;
  16. use OCP\IURLGenerator;
  17. use OCP\IUserManager;
  18. use OCP\User\Events\UserChangedEvent;
  19. class CloudIdManager implements ICloudIdManager {
  20. /** @var IManager */
  21. private $contactsManager;
  22. /** @var IURLGenerator */
  23. private $urlGenerator;
  24. /** @var IUserManager */
  25. private $userManager;
  26. private ICache $memCache;
  27. /** @var array[] */
  28. private array $cache = [];
  29. public function __construct(
  30. IManager $contactsManager,
  31. IURLGenerator $urlGenerator,
  32. IUserManager $userManager,
  33. ICacheFactory $cacheFactory,
  34. IEventDispatcher $eventDispatcher
  35. ) {
  36. $this->contactsManager = $contactsManager;
  37. $this->urlGenerator = $urlGenerator;
  38. $this->userManager = $userManager;
  39. $this->memCache = $cacheFactory->createDistributed('cloud_id_');
  40. $eventDispatcher->addListener(UserChangedEvent::class, [$this, 'handleUserEvent']);
  41. $eventDispatcher->addListener(CardUpdatedEvent::class, [$this, 'handleCardEvent']);
  42. }
  43. public function handleUserEvent(Event $event): void {
  44. if ($event instanceof UserChangedEvent && $event->getFeature() === 'displayName') {
  45. $userId = $event->getUser()->getUID();
  46. $key = $userId . '@local';
  47. unset($this->cache[$key]);
  48. $this->memCache->remove($key);
  49. }
  50. }
  51. public function handleCardEvent(Event $event): void {
  52. if ($event instanceof CardUpdatedEvent) {
  53. $data = $event->getCardData()['carddata'];
  54. foreach (explode("\r\n", $data) as $line) {
  55. if (str_starts_with($line, "CLOUD;")) {
  56. $parts = explode(':', $line, 2);
  57. if (isset($parts[1])) {
  58. $key = $parts[1];
  59. unset($this->cache[$key]);
  60. $this->memCache->remove($key);
  61. }
  62. }
  63. }
  64. }
  65. }
  66. /**
  67. * @param string $cloudId
  68. * @return ICloudId
  69. * @throws \InvalidArgumentException
  70. */
  71. public function resolveCloudId(string $cloudId): ICloudId {
  72. // TODO magic here to get the url and user instead of just splitting on @
  73. if (!$this->isValidCloudId($cloudId)) {
  74. throw new \InvalidArgumentException('Invalid cloud id');
  75. }
  76. // Find the first character that is not allowed in user names
  77. $id = $this->fixRemoteURL($cloudId);
  78. $posSlash = strpos($id, '/');
  79. $posColon = strpos($id, ':');
  80. if ($posSlash === false && $posColon === false) {
  81. $invalidPos = \strlen($id);
  82. } elseif ($posSlash === false) {
  83. $invalidPos = $posColon;
  84. } elseif ($posColon === false) {
  85. $invalidPos = $posSlash;
  86. } else {
  87. $invalidPos = min($posSlash, $posColon);
  88. }
  89. $lastValidAtPos = strrpos($id, '@', $invalidPos - strlen($id));
  90. if ($lastValidAtPos !== false) {
  91. $user = substr($id, 0, $lastValidAtPos);
  92. $remote = substr($id, $lastValidAtPos + 1);
  93. $this->userManager->validateUserId($user);
  94. if (!empty($user) && !empty($remote)) {
  95. return new CloudId($id, $user, $remote, $this->getDisplayNameFromContact($id));
  96. }
  97. }
  98. throw new \InvalidArgumentException('Invalid cloud id');
  99. }
  100. protected function getDisplayNameFromContact(string $cloudId): ?string {
  101. $addressBookEntries = $this->contactsManager->search($cloudId, ['CLOUD'], [
  102. 'limit' => 1,
  103. 'enumeration' => false,
  104. 'fullmatch' => false,
  105. 'strict_search' => true,
  106. ]);
  107. foreach ($addressBookEntries as $entry) {
  108. if (isset($entry['CLOUD'])) {
  109. foreach ($entry['CLOUD'] as $cloudID) {
  110. if ($cloudID === $cloudId) {
  111. // Warning, if user decides to make his full name local only,
  112. // no FN is found on federated servers
  113. if (isset($entry['FN'])) {
  114. return $entry['FN'];
  115. } else {
  116. return $cloudID;
  117. }
  118. }
  119. }
  120. }
  121. }
  122. return null;
  123. }
  124. /**
  125. * @param string $user
  126. * @param string|null $remote
  127. * @return CloudId
  128. */
  129. public function getCloudId(string $user, ?string $remote): ICloudId {
  130. $isLocal = $remote === null;
  131. if ($isLocal) {
  132. $remote = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
  133. }
  134. // note that for remote id's we don't strip the protocol for the remote we use to construct the CloudId
  135. // this way if a user has an explicit non-https cloud id this will be preserved
  136. // we do still use the version without protocol for looking up the display name
  137. $remote = $this->fixRemoteURL($remote);
  138. $host = $this->removeProtocolFromUrl($remote);
  139. $key = $user . '@' . ($isLocal ? 'local' : $host);
  140. $cached = $this->cache[$key] ?? $this->memCache->get($key);
  141. if ($cached) {
  142. $this->cache[$key] = $cached; // put items from memcache into local cache
  143. return new CloudId($cached['id'], $cached['user'], $cached['remote'], $cached['displayName']);
  144. }
  145. if ($isLocal) {
  146. $localUser = $this->userManager->get($user);
  147. $displayName = $localUser ? $localUser->getDisplayName() : '';
  148. } else {
  149. $displayName = $this->getDisplayNameFromContact($user . '@' . $host);
  150. }
  151. // For the visible cloudID we only strip away https
  152. $id = $user . '@' . $this->removeProtocolFromUrl($remote, true);
  153. $data = [
  154. 'id' => $id,
  155. 'user' => $user,
  156. 'remote' => $remote,
  157. 'displayName' => $displayName,
  158. ];
  159. $this->cache[$key] = $data;
  160. $this->memCache->set($key, $data, 15 * 60);
  161. return new CloudId($id, $user, $remote, $displayName);
  162. }
  163. /**
  164. * @param string $url
  165. * @return string
  166. */
  167. public function removeProtocolFromUrl(string $url, bool $httpsOnly = false): string {
  168. if (str_starts_with($url, 'https://')) {
  169. return substr($url, 8);
  170. }
  171. if (!$httpsOnly && str_starts_with($url, 'http://')) {
  172. return substr($url, 7);
  173. }
  174. return $url;
  175. }
  176. /**
  177. * Strips away a potential file names and trailing slashes:
  178. * - http://localhost
  179. * - http://localhost/
  180. * - http://localhost/index.php
  181. * - http://localhost/index.php/s/{shareToken}
  182. *
  183. * all return: http://localhost
  184. *
  185. * @param string $remote
  186. * @return string
  187. */
  188. protected function fixRemoteURL(string $remote): string {
  189. $remote = str_replace('\\', '/', $remote);
  190. if ($fileNamePosition = strpos($remote, '/index.php')) {
  191. $remote = substr($remote, 0, $fileNamePosition);
  192. }
  193. $remote = rtrim($remote, '/');
  194. return $remote;
  195. }
  196. /**
  197. * @param string $cloudId
  198. * @return bool
  199. */
  200. public function isValidCloudId(string $cloudId): bool {
  201. return str_contains($cloudId, '@');
  202. }
  203. }