ContactsStore.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. <?php
  2. /**
  3. * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
  4. * @copyright 2017 Lukas Reschke <lukas@statuscode.ch>
  5. *
  6. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Georg Ehrke <oc.list@georgehrke.com>
  9. * @author Joas Schilling <coding@schilljs.com>
  10. * @author Lukas Reschke <lukas@statuscode.ch>
  11. * @author Roeland Jago Douma <roeland@famdouma.nl>
  12. * @author Tobia De Koninck <tobia@ledfan.be>
  13. *
  14. * @license GNU AGPL version 3 or any later version
  15. *
  16. * This program is free software: you can redistribute it and/or modify
  17. * it under the terms of the GNU Affero General Public License as
  18. * published by the Free Software Foundation, either version 3 of the
  19. * License, or (at your option) any later version.
  20. *
  21. * This program is distributed in the hope that it will be useful,
  22. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. * GNU Affero General Public License for more details.
  25. *
  26. * You should have received a copy of the GNU Affero General Public License
  27. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  28. *
  29. */
  30. namespace OC\Contacts\ContactsMenu;
  31. use OC\KnownUser\KnownUserService;
  32. use OC\Profile\ProfileManager;
  33. use OCP\Contacts\ContactsMenu\IContactsStore;
  34. use OCP\Contacts\ContactsMenu\IEntry;
  35. use OCP\Contacts\IManager;
  36. use OCP\IConfig;
  37. use OCP\IGroupManager;
  38. use OCP\IURLGenerator;
  39. use OCP\IUser;
  40. use OCP\IUserManager;
  41. use OCP\L10N\IFactory as IL10NFactory;
  42. class ContactsStore implements IContactsStore {
  43. private IManager $contactsManager;
  44. private IConfig $config;
  45. private ProfileManager $profileManager;
  46. private IUserManager $userManager;
  47. private IURLGenerator $urlGenerator;
  48. private IGroupManager $groupManager;
  49. private KnownUserService $knownUserService;
  50. private IL10NFactory $l10nFactory;
  51. public function __construct(
  52. IManager $contactsManager,
  53. IConfig $config,
  54. ProfileManager $profileManager,
  55. IUserManager $userManager,
  56. IURLGenerator $urlGenerator,
  57. IGroupManager $groupManager,
  58. KnownUserService $knownUserService,
  59. IL10NFactory $l10nFactory
  60. ) {
  61. $this->contactsManager = $contactsManager;
  62. $this->config = $config;
  63. $this->profileManager = $profileManager;
  64. $this->userManager = $userManager;
  65. $this->urlGenerator = $urlGenerator;
  66. $this->groupManager = $groupManager;
  67. $this->knownUserService = $knownUserService;
  68. $this->l10nFactory = $l10nFactory;
  69. }
  70. /**
  71. * @return IEntry[]
  72. */
  73. public function getContacts(IUser $user, ?string $filter, ?int $limit = null, ?int $offset = null): array {
  74. $options = [
  75. 'enumeration' => $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes',
  76. 'fullmatch' => $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes',
  77. ];
  78. if ($limit !== null) {
  79. $options['limit'] = $limit;
  80. }
  81. if ($offset !== null) {
  82. $options['offset'] = $offset;
  83. }
  84. $allContacts = $this->contactsManager->search(
  85. $filter ?? '',
  86. [
  87. 'FN',
  88. 'EMAIL'
  89. ],
  90. $options
  91. );
  92. $userId = $user->getUID();
  93. $contacts = array_filter($allContacts, function ($contact) use ($userId) {
  94. // When searching for multiple results, we strip out the current user
  95. if (array_key_exists('UID', $contact)) {
  96. return $contact['UID'] !== $userId;
  97. }
  98. return true;
  99. });
  100. $entries = array_map(function (array $contact) {
  101. return $this->contactArrayToEntry($contact);
  102. }, $contacts);
  103. return $this->filterContacts(
  104. $user,
  105. $entries,
  106. $filter
  107. );
  108. }
  109. /**
  110. * Filters the contacts. Applied filters:
  111. * 1. if the `shareapi_allow_share_dialog_user_enumeration` config option is
  112. * enabled it will filter all local users
  113. * 2. if the `shareapi_exclude_groups` config option is enabled and the
  114. * current user is in an excluded group it will filter all local users.
  115. * 3. if the `shareapi_only_share_with_group_members` config option is
  116. * enabled it will filter all users which doesn't have a common group
  117. * with the current user.
  118. *
  119. * @param IUser $self
  120. * @param Entry[] $entries
  121. * @param string|null $filter
  122. * @return Entry[] the filtered contacts
  123. */
  124. private function filterContacts(
  125. IUser $self,
  126. array $entries,
  127. ?string $filter
  128. ): array {
  129. $disallowEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') !== 'yes';
  130. $restrictEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
  131. $restrictEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
  132. $allowEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
  133. $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups', 'no') === 'yes';
  134. // whether to filter out local users
  135. $skipLocal = false;
  136. // whether to filter out all users which don't have a common group as the current user
  137. $ownGroupsOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
  138. $selfGroups = $this->groupManager->getUserGroupIds($self);
  139. if ($excludedGroups) {
  140. $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', '');
  141. $decodedExcludeGroups = json_decode($excludedGroups, true);
  142. $excludeGroupsList = $decodedExcludeGroups ?? [];
  143. if (count(array_intersect($excludeGroupsList, $selfGroups)) !== 0) {
  144. // a group of the current user is excluded -> filter all local users
  145. $skipLocal = true;
  146. }
  147. }
  148. $selfUID = $self->getUID();
  149. return array_values(array_filter($entries, function (IEntry $entry) use ($skipLocal, $ownGroupsOnly, $selfGroups, $selfUID, $disallowEnumeration, $restrictEnumerationGroup, $restrictEnumerationPhone, $allowEnumerationFullMatch, $filter) {
  150. if ($entry->getProperty('isLocalSystemBook')) {
  151. if ($skipLocal) {
  152. return false;
  153. }
  154. $checkedCommonGroupAlready = false;
  155. // Prevent enumerating local users
  156. if ($disallowEnumeration) {
  157. if (!$allowEnumerationFullMatch) {
  158. return false;
  159. }
  160. $filterOutUser = true;
  161. $mailAddresses = $entry->getEMailAddresses();
  162. foreach ($mailAddresses as $mailAddress) {
  163. if ($mailAddress === $filter) {
  164. $filterOutUser = false;
  165. break;
  166. }
  167. }
  168. if ($entry->getProperty('UID') && $entry->getProperty('UID') === $filter) {
  169. $filterOutUser = false;
  170. }
  171. if ($filterOutUser) {
  172. return false;
  173. }
  174. } elseif ($restrictEnumerationPhone || $restrictEnumerationGroup) {
  175. $canEnumerate = false;
  176. if ($restrictEnumerationPhone) {
  177. $canEnumerate = $this->knownUserService->isKnownToUser($selfUID, $entry->getProperty('UID'));
  178. }
  179. if (!$canEnumerate && $restrictEnumerationGroup) {
  180. $user = $this->userManager->get($entry->getProperty('UID'));
  181. if ($user === null) {
  182. return false;
  183. }
  184. $contactGroups = $this->groupManager->getUserGroupIds($user);
  185. $canEnumerate = !empty(array_intersect($contactGroups, $selfGroups));
  186. $checkedCommonGroupAlready = true;
  187. }
  188. if (!$canEnumerate) {
  189. return false;
  190. }
  191. }
  192. if ($ownGroupsOnly && !$checkedCommonGroupAlready) {
  193. $user = $this->userManager->get($entry->getProperty('UID'));
  194. if (!$user instanceof IUser) {
  195. return false;
  196. }
  197. $contactGroups = $this->groupManager->getUserGroupIds($user);
  198. if (empty(array_intersect($contactGroups, $selfGroups))) {
  199. // no groups in common, so shouldn't see the contact
  200. return false;
  201. }
  202. }
  203. }
  204. return true;
  205. }));
  206. }
  207. public function findOne(IUser $user, int $shareType, string $shareWith): ?IEntry {
  208. switch ($shareType) {
  209. case 0:
  210. case 6:
  211. $filter = ['UID'];
  212. break;
  213. case 4:
  214. $filter = ['EMAIL'];
  215. break;
  216. default:
  217. return null;
  218. }
  219. $contacts = $this->contactsManager->search($shareWith, $filter, [
  220. 'strict_search' => true,
  221. ]);
  222. $match = null;
  223. foreach ($contacts as $contact) {
  224. if ($shareType === 4 && isset($contact['EMAIL'])) {
  225. if (in_array($shareWith, $contact['EMAIL'])) {
  226. $match = $contact;
  227. break;
  228. }
  229. }
  230. if ($shareType === 0 || $shareType === 6) {
  231. $isLocal = $contact['isLocalSystemBook'] ?? false;
  232. if ($contact['UID'] === $shareWith && $isLocal === true) {
  233. $match = $contact;
  234. break;
  235. }
  236. }
  237. }
  238. if ($match) {
  239. $match = $this->filterContacts($user, [$this->contactArrayToEntry($match)], $shareWith);
  240. if (count($match) === 1) {
  241. $match = $match[0];
  242. } else {
  243. $match = null;
  244. }
  245. }
  246. return $match;
  247. }
  248. private function contactArrayToEntry(array $contact): Entry {
  249. $entry = new Entry();
  250. if (isset($contact['UID'])) {
  251. $uid = $contact['UID'];
  252. $entry->setId($uid);
  253. if (isset($contact['isLocalSystemBook'])) {
  254. $avatar = $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $uid, 'size' => 64]);
  255. } elseif (isset($contact['FN'])) {
  256. $avatar = $this->urlGenerator->linkToRouteAbsolute('core.GuestAvatar.getAvatar', ['guestName' => $contact['FN'], 'size' => 64]);
  257. } else {
  258. $avatar = $this->urlGenerator->linkToRouteAbsolute('core.GuestAvatar.getAvatar', ['guestName' => $uid, 'size' => 64]);
  259. }
  260. $entry->setAvatar($avatar);
  261. }
  262. if (isset($contact['FN'])) {
  263. $entry->setFullName($contact['FN']);
  264. }
  265. $avatarPrefix = "VALUE=uri:";
  266. if (isset($contact['PHOTO']) && str_starts_with($contact['PHOTO'], $avatarPrefix)) {
  267. $entry->setAvatar(substr($contact['PHOTO'], strlen($avatarPrefix)));
  268. }
  269. if (isset($contact['EMAIL'])) {
  270. foreach ($contact['EMAIL'] as $email) {
  271. $entry->addEMailAddress($email);
  272. }
  273. }
  274. // Provide profile parameters for core/src/OC/contactsmenu/contact.handlebars template
  275. if (isset($contact['UID']) && isset($contact['FN'])) {
  276. $targetUserId = $contact['UID'];
  277. $targetUser = $this->userManager->get($targetUserId);
  278. if (!empty($targetUser)) {
  279. if ($this->profileManager->isProfileEnabled($targetUser)) {
  280. $entry->setProfileTitle($this->l10nFactory->get('lib')->t('View profile'));
  281. $entry->setProfileUrl($this->urlGenerator->linkToRouteAbsolute('core.ProfilePage.index', ['targetUserId' => $targetUserId]));
  282. }
  283. }
  284. }
  285. // Attach all other properties to the entry too because some
  286. // providers might make use of it.
  287. $entry->setProperties($contact);
  288. return $entry;
  289. }
  290. }