Converter.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\DAV\CardDAV;
  8. use DateTimeImmutable;
  9. use Exception;
  10. use OCP\Accounts\IAccountManager;
  11. use OCP\IImage;
  12. use OCP\IURLGenerator;
  13. use OCP\IUser;
  14. use OCP\IUserManager;
  15. use Psr\Log\LoggerInterface;
  16. use Sabre\VObject\Component\VCard;
  17. use Sabre\VObject\Property\Text;
  18. use Sabre\VObject\Property\VCard\Date;
  19. class Converter {
  20. public function __construct(
  21. private IAccountManager $accountManager,
  22. private IUserManager $userManager,
  23. private IURLGenerator $urlGenerator,
  24. private LoggerInterface $logger,
  25. ) {
  26. }
  27. public function createCardFromUser(IUser $user): ?VCard {
  28. $userProperties = $this->accountManager->getAccount($user)->getAllProperties();
  29. $uid = $user->getUID();
  30. $cloudId = $user->getCloudId();
  31. $image = $this->getAvatarImage($user);
  32. $vCard = new VCard();
  33. $vCard->VERSION = '3.0';
  34. $vCard->UID = $uid;
  35. $publish = false;
  36. foreach ($userProperties as $property) {
  37. if ($property->getName() !== IAccountManager::PROPERTY_AVATAR && empty($property->getValue())) {
  38. continue;
  39. }
  40. $scope = $property->getScope();
  41. // Do not write private data to the system address book at all
  42. if ($scope === IAccountManager::SCOPE_PRIVATE || empty($scope)) {
  43. continue;
  44. }
  45. $publish = true;
  46. switch ($property->getName()) {
  47. case IAccountManager::PROPERTY_DISPLAYNAME:
  48. $vCard->add(new Text($vCard, 'FN', $property->getValue(), ['X-NC-SCOPE' => $scope]));
  49. $vCard->add(new Text($vCard, 'N', $this->splitFullName($property->getValue()), ['X-NC-SCOPE' => $scope]));
  50. break;
  51. case IAccountManager::PROPERTY_AVATAR:
  52. if ($image !== null) {
  53. $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType(), ['X-NC-SCOPE' => $scope]]);
  54. }
  55. break;
  56. case IAccountManager::COLLECTION_EMAIL:
  57. case IAccountManager::PROPERTY_EMAIL:
  58. $vCard->add(new Text($vCard, 'EMAIL', $property->getValue(), ['TYPE' => 'OTHER', 'X-NC-SCOPE' => $scope]));
  59. break;
  60. case IAccountManager::PROPERTY_WEBSITE:
  61. $vCard->add(new Text($vCard, 'URL', $property->getValue(), ['X-NC-SCOPE' => $scope]));
  62. break;
  63. case IAccountManager::PROPERTY_PROFILE_ENABLED:
  64. if ($property->getValue()) {
  65. $vCard->add(
  66. new Text(
  67. $vCard,
  68. 'X-SOCIALPROFILE',
  69. $this->urlGenerator->linkToRouteAbsolute('profile.ProfilePage.index', ['targetUserId' => $user->getUID()]),
  70. [
  71. 'TYPE' => 'NEXTCLOUD',
  72. 'X-NC-SCOPE' => IAccountManager::SCOPE_PUBLISHED
  73. ]
  74. )
  75. );
  76. }
  77. break;
  78. case IAccountManager::PROPERTY_PHONE:
  79. $vCard->add(new Text($vCard, 'TEL', $property->getValue(), ['TYPE' => 'VOICE', 'X-NC-SCOPE' => $scope]));
  80. break;
  81. case IAccountManager::PROPERTY_ADDRESS:
  82. // structured prop: https://www.rfc-editor.org/rfc/rfc6350.html#section-6.3.1
  83. // post office box;extended address;street address;locality;region;postal code;country
  84. $vCard->add(
  85. new Text(
  86. $vCard,
  87. 'ADR',
  88. [ '', '', '', $property->getValue(), '', '', '' ],
  89. [
  90. 'TYPE' => 'OTHER',
  91. 'X-NC-SCOPE' => $scope,
  92. ]
  93. )
  94. );
  95. break;
  96. case IAccountManager::PROPERTY_TWITTER:
  97. $vCard->add(new Text($vCard, 'X-SOCIALPROFILE', $property->getValue(), ['TYPE' => 'TWITTER', 'X-NC-SCOPE' => $scope]));
  98. break;
  99. case IAccountManager::PROPERTY_ORGANISATION:
  100. $vCard->add(new Text($vCard, 'ORG', $property->getValue(), ['X-NC-SCOPE' => $scope]));
  101. break;
  102. case IAccountManager::PROPERTY_ROLE:
  103. $vCard->add(new Text($vCard, 'TITLE', $property->getValue(), ['X-NC-SCOPE' => $scope]));
  104. break;
  105. case IAccountManager::PROPERTY_BIOGRAPHY:
  106. $vCard->add(new Text($vCard, 'NOTE', $property->getValue(), ['X-NC-SCOPE' => $scope]));
  107. break;
  108. case IAccountManager::PROPERTY_BIRTHDATE:
  109. try {
  110. $birthdate = new DateTimeImmutable($property->getValue());
  111. } catch (Exception $e) {
  112. // Invalid date -> just skip the property
  113. $this->logger->info("Failed to parse user's birthdate for the SAB: " . $property->getValue(), [
  114. 'exception' => $e,
  115. 'userId' => $user->getUID(),
  116. ]);
  117. break;
  118. }
  119. $dateProperty = new Date($vCard, 'BDAY', null, ['X-NC-SCOPE' => $scope]);
  120. $dateProperty->setDateTime($birthdate);
  121. $vCard->add($dateProperty);
  122. break;
  123. }
  124. }
  125. // Local properties
  126. $managers = $user->getManagerUids();
  127. // X-MANAGERSNAME only allows a single value, so we take the first manager
  128. if (isset($managers[0])) {
  129. $displayName = $this->userManager->getDisplayName($managers[0]);
  130. // Only set the manager if a user object is found
  131. if ($displayName !== null) {
  132. $vCard->add(new Text($vCard, 'X-MANAGERSNAME', $displayName, [
  133. 'uid' => $managers[0],
  134. 'X-NC-SCOPE' => IAccountManager::SCOPE_LOCAL,
  135. ]));
  136. }
  137. }
  138. if ($publish && !empty($cloudId)) {
  139. $vCard->add(new Text($vCard, 'CLOUD', $cloudId));
  140. $vCard->validate();
  141. return $vCard;
  142. }
  143. return null;
  144. }
  145. public function splitFullName(string $fullName): array {
  146. // Very basic western style parsing. I'm not gonna implement
  147. // https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;)
  148. $elements = explode(' ', $fullName);
  149. $result = ['', '', '', '', ''];
  150. if (count($elements) > 2) {
  151. $result[0] = implode(' ', array_slice($elements, count($elements) - 1));
  152. $result[1] = $elements[0];
  153. $result[2] = implode(' ', array_slice($elements, 1, count($elements) - 2));
  154. } elseif (count($elements) === 2) {
  155. $result[0] = $elements[1];
  156. $result[1] = $elements[0];
  157. } else {
  158. $result[0] = $elements[0];
  159. }
  160. return $result;
  161. }
  162. private function getAvatarImage(IUser $user): ?IImage {
  163. try {
  164. return $user->getAvatarImage(512);
  165. } catch (Exception $ex) {
  166. return null;
  167. }
  168. }
  169. }