CloudIdManager.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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->stripShareLinkFragments($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. $remote = $this->ensureDefaultProtocol($remote);
  96. return new CloudId($id, $user, $remote, $this->getDisplayNameFromContact($id));
  97. }
  98. }
  99. throw new \InvalidArgumentException('Invalid cloud id');
  100. }
  101. protected function getDisplayNameFromContact(string $cloudId): ?string {
  102. $addressBookEntries = $this->contactsManager->search($cloudId, ['CLOUD'], [
  103. 'limit' => 1,
  104. 'enumeration' => false,
  105. 'fullmatch' => false,
  106. 'strict_search' => true,
  107. ]);
  108. foreach ($addressBookEntries as $entry) {
  109. if (isset($entry['CLOUD'])) {
  110. foreach ($entry['CLOUD'] as $cloudID) {
  111. if ($cloudID === $cloudId) {
  112. // Warning, if user decides to make their full name local only,
  113. // no FN is found on federated servers
  114. if (isset($entry['FN'])) {
  115. return $entry['FN'];
  116. } else {
  117. return $cloudID;
  118. }
  119. }
  120. }
  121. }
  122. }
  123. return null;
  124. }
  125. /**
  126. * @param string $user
  127. * @param string|null $remote
  128. * @return CloudId
  129. */
  130. public function getCloudId(string $user, ?string $remote): ICloudId {
  131. $isLocal = $remote === null;
  132. if ($isLocal) {
  133. $remote = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
  134. }
  135. // note that for remote id's we don't strip the protocol for the remote we use to construct the CloudId
  136. // this way if a user has an explicit non-https cloud id this will be preserved
  137. // we do still use the version without protocol for looking up the display name
  138. $remote = $this->stripShareLinkFragments($remote);
  139. $host = $this->removeProtocolFromUrl($remote);
  140. $remote = $this->ensureDefaultProtocol($remote);
  141. $key = $user . '@' . ($isLocal ? 'local' : $host);
  142. $cached = $this->cache[$key] ?? $this->memCache->get($key);
  143. if ($cached) {
  144. $this->cache[$key] = $cached; // put items from memcache into local cache
  145. return new CloudId($cached['id'], $cached['user'], $cached['remote'], $cached['displayName']);
  146. }
  147. if ($isLocal) {
  148. $localUser = $this->userManager->get($user);
  149. $displayName = $localUser ? $localUser->getDisplayName() : '';
  150. } else {
  151. $displayName = $this->getDisplayNameFromContact($user . '@' . $host);
  152. }
  153. // For the visible cloudID we only strip away https
  154. $id = $user . '@' . $this->removeProtocolFromUrl($remote, true);
  155. $data = [
  156. 'id' => $id,
  157. 'user' => $user,
  158. 'remote' => $remote,
  159. 'displayName' => $displayName,
  160. ];
  161. $this->cache[$key] = $data;
  162. $this->memCache->set($key, $data, 15 * 60);
  163. return new CloudId($id, $user, $remote, $displayName);
  164. }
  165. /**
  166. * @param string $url
  167. * @return string
  168. */
  169. public function removeProtocolFromUrl(string $url, bool $httpsOnly = false): string {
  170. if (str_starts_with($url, 'https://')) {
  171. return substr($url, 8);
  172. }
  173. if (!$httpsOnly && str_starts_with($url, 'http://')) {
  174. return substr($url, 7);
  175. }
  176. return $url;
  177. }
  178. protected function ensureDefaultProtocol(string $remote): string {
  179. if (!str_contains($remote, '://')) {
  180. $remote = 'https://' . $remote;
  181. }
  182. return $remote;
  183. }
  184. /**
  185. * Strips away a potential file names and trailing slashes:
  186. * - http://localhost
  187. * - http://localhost/
  188. * - http://localhost/index.php
  189. * - http://localhost/index.php/s/{shareToken}
  190. *
  191. * all return: http://localhost
  192. *
  193. * @param string $remote
  194. * @return string
  195. */
  196. protected function stripShareLinkFragments(string $remote): string {
  197. $remote = str_replace('\\', '/', $remote);
  198. if ($fileNamePosition = strpos($remote, '/index.php')) {
  199. $remote = substr($remote, 0, $fileNamePosition);
  200. }
  201. $remote = rtrim($remote, '/');
  202. return $remote;
  203. }
  204. /**
  205. * @param string $cloudId
  206. * @return bool
  207. */
  208. public function isValidCloudId(string $cloudId): bool {
  209. return str_contains($cloudId, '@');
  210. }
  211. }