Converter.php 6.1 KB

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