Backend.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\DAV\CardDAV\Activity;
  8. use OCA\DAV\CardDAV\Activity\Provider\Addressbook;
  9. use OCP\Activity\IEvent;
  10. use OCP\Activity\IManager as IActivityManager;
  11. use OCP\App\IAppManager;
  12. use OCP\IGroup;
  13. use OCP\IGroupManager;
  14. use OCP\IUser;
  15. use OCP\IUserManager;
  16. use OCP\IUserSession;
  17. use Sabre\CardDAV\Plugin;
  18. use Sabre\VObject\Reader;
  19. class Backend {
  20. public function __construct(
  21. protected IActivityManager $activityManager,
  22. protected IGroupManager $groupManager,
  23. protected IUserSession $userSession,
  24. protected IAppManager $appManager,
  25. protected IUserManager $userManager,
  26. ) {
  27. }
  28. /**
  29. * Creates activities when an addressbook was creates
  30. *
  31. * @param array $addressbookData
  32. */
  33. public function onAddressbookCreate(array $addressbookData): void {
  34. $this->triggerAddressbookActivity(Addressbook::SUBJECT_ADD, $addressbookData);
  35. }
  36. /**
  37. * Creates activities when a calendar was updated
  38. *
  39. * @param array $addressbookData
  40. * @param array $shares
  41. * @param array $properties
  42. */
  43. public function onAddressbookUpdate(array $addressbookData, array $shares, array $properties): void {
  44. $this->triggerAddressbookActivity(Addressbook::SUBJECT_UPDATE, $addressbookData, $shares, $properties);
  45. }
  46. /**
  47. * Creates activities when a calendar was deleted
  48. *
  49. * @param array $addressbookData
  50. * @param array $shares
  51. */
  52. public function onAddressbookDelete(array $addressbookData, array $shares): void {
  53. $this->triggerAddressbookActivity(Addressbook::SUBJECT_DELETE, $addressbookData, $shares);
  54. }
  55. /**
  56. * Creates activities for all related users when a calendar was touched
  57. *
  58. * @param string $action
  59. * @param array $addressbookData
  60. * @param array $shares
  61. * @param array $changedProperties
  62. */
  63. protected function triggerAddressbookActivity(string $action, array $addressbookData, array $shares = [], array $changedProperties = []): void {
  64. if (!isset($addressbookData['principaluri'])) {
  65. return;
  66. }
  67. $principalUri = $addressbookData['principaluri'];
  68. // We are not interested in changes from the system addressbook
  69. if ($principalUri === 'principals/system/system') {
  70. return;
  71. }
  72. $principal = explode('/', $principalUri);
  73. $owner = array_pop($principal);
  74. $currentUser = $this->userSession->getUser();
  75. if ($currentUser instanceof IUser) {
  76. $currentUser = $currentUser->getUID();
  77. } else {
  78. $currentUser = $owner;
  79. }
  80. $event = $this->activityManager->generateEvent();
  81. $event->setApp('dav')
  82. ->setObject('addressbook', (int)$addressbookData['id'])
  83. ->setType('contacts')
  84. ->setAuthor($currentUser);
  85. $changedVisibleInformation = array_intersect([
  86. '{DAV:}displayname',
  87. '{' . Plugin::NS_CARDDAV . '}addressbook-description',
  88. ], array_keys($changedProperties));
  89. if (empty($shares) || ($action === Addressbook::SUBJECT_UPDATE && empty($changedVisibleInformation))) {
  90. $users = [$owner];
  91. } else {
  92. $users = $this->getUsersForShares($shares);
  93. $users[] = $owner;
  94. }
  95. foreach ($users as $user) {
  96. if ($action === Addressbook::SUBJECT_DELETE && !$this->userManager->userExists($user)) {
  97. // Avoid creating addressbook_delete activities for deleted users
  98. continue;
  99. }
  100. $event->setAffectedUser($user)
  101. ->setSubject(
  102. $user === $currentUser ? $action . '_self' : $action,
  103. [
  104. 'actor' => $currentUser,
  105. 'addressbook' => [
  106. 'id' => (int)$addressbookData['id'],
  107. 'uri' => $addressbookData['uri'],
  108. 'name' => $addressbookData['{DAV:}displayname'],
  109. ],
  110. ]
  111. );
  112. $this->activityManager->publish($event);
  113. }
  114. }
  115. /**
  116. * Creates activities for all related users when an addressbook was (un-)shared
  117. *
  118. * @param array $addressbookData
  119. * @param array $shares
  120. * @param array $add
  121. * @param array $remove
  122. */
  123. public function onAddressbookUpdateShares(array $addressbookData, array $shares, array $add, array $remove): void {
  124. $principal = explode('/', $addressbookData['principaluri']);
  125. $owner = $principal[2];
  126. $currentUser = $this->userSession->getUser();
  127. if ($currentUser instanceof IUser) {
  128. $currentUser = $currentUser->getUID();
  129. } else {
  130. $currentUser = $owner;
  131. }
  132. $event = $this->activityManager->generateEvent();
  133. $event->setApp('dav')
  134. ->setObject('addressbook', (int)$addressbookData['id'])
  135. ->setType('contacts')
  136. ->setAuthor($currentUser);
  137. foreach ($remove as $principal) {
  138. // principal:principals/users/test
  139. $parts = explode(':', $principal, 2);
  140. if ($parts[0] !== 'principal') {
  141. continue;
  142. }
  143. $principal = explode('/', $parts[1]);
  144. if ($principal[1] === 'users') {
  145. $this->triggerActivityUser(
  146. $principal[2],
  147. $event,
  148. $addressbookData,
  149. Addressbook::SUBJECT_UNSHARE_USER,
  150. Addressbook::SUBJECT_DELETE . '_self'
  151. );
  152. if ($owner !== $principal[2]) {
  153. $parameters = [
  154. 'actor' => $event->getAuthor(),
  155. 'addressbook' => [
  156. 'id' => (int)$addressbookData['id'],
  157. 'uri' => $addressbookData['uri'],
  158. 'name' => $addressbookData['{DAV:}displayname'],
  159. ],
  160. 'user' => $principal[2],
  161. ];
  162. if ($owner === $event->getAuthor()) {
  163. $subject = Addressbook::SUBJECT_UNSHARE_USER . '_you';
  164. } elseif ($principal[2] === $event->getAuthor()) {
  165. $subject = Addressbook::SUBJECT_UNSHARE_USER . '_self';
  166. } else {
  167. $event->setAffectedUser($event->getAuthor())
  168. ->setSubject(Addressbook::SUBJECT_UNSHARE_USER . '_you', $parameters);
  169. $this->activityManager->publish($event);
  170. $subject = Addressbook::SUBJECT_UNSHARE_USER . '_by';
  171. }
  172. $event->setAffectedUser($owner)
  173. ->setSubject($subject, $parameters);
  174. $this->activityManager->publish($event);
  175. }
  176. } elseif ($principal[1] === 'groups') {
  177. $this->triggerActivityGroup($principal[2], $event, $addressbookData, Addressbook::SUBJECT_UNSHARE_USER);
  178. $parameters = [
  179. 'actor' => $event->getAuthor(),
  180. 'addressbook' => [
  181. 'id' => (int)$addressbookData['id'],
  182. 'uri' => $addressbookData['uri'],
  183. 'name' => $addressbookData['{DAV:}displayname'],
  184. ],
  185. 'group' => $principal[2],
  186. ];
  187. if ($owner === $event->getAuthor()) {
  188. $subject = Addressbook::SUBJECT_UNSHARE_GROUP . '_you';
  189. } else {
  190. $event->setAffectedUser($event->getAuthor())
  191. ->setSubject(Addressbook::SUBJECT_UNSHARE_GROUP . '_you', $parameters);
  192. $this->activityManager->publish($event);
  193. $subject = Addressbook::SUBJECT_UNSHARE_GROUP . '_by';
  194. }
  195. $event->setAffectedUser($owner)
  196. ->setSubject($subject, $parameters);
  197. $this->activityManager->publish($event);
  198. }
  199. }
  200. foreach ($add as $share) {
  201. if ($this->isAlreadyShared($share['href'], $shares)) {
  202. continue;
  203. }
  204. // principal:principals/users/test
  205. $parts = explode(':', $share['href'], 2);
  206. if ($parts[0] !== 'principal') {
  207. continue;
  208. }
  209. $principal = explode('/', $parts[1]);
  210. if ($principal[1] === 'users') {
  211. $this->triggerActivityUser($principal[2], $event, $addressbookData, Addressbook::SUBJECT_SHARE_USER);
  212. if ($owner !== $principal[2]) {
  213. $parameters = [
  214. 'actor' => $event->getAuthor(),
  215. 'addressbook' => [
  216. 'id' => (int)$addressbookData['id'],
  217. 'uri' => $addressbookData['uri'],
  218. 'name' => $addressbookData['{DAV:}displayname'],
  219. ],
  220. 'user' => $principal[2],
  221. ];
  222. if ($owner === $event->getAuthor()) {
  223. $subject = Addressbook::SUBJECT_SHARE_USER . '_you';
  224. } else {
  225. $event->setAffectedUser($event->getAuthor())
  226. ->setSubject(Addressbook::SUBJECT_SHARE_USER . '_you', $parameters);
  227. $this->activityManager->publish($event);
  228. $subject = Addressbook::SUBJECT_SHARE_USER . '_by';
  229. }
  230. $event->setAffectedUser($owner)
  231. ->setSubject($subject, $parameters);
  232. $this->activityManager->publish($event);
  233. }
  234. } elseif ($principal[1] === 'groups') {
  235. $this->triggerActivityGroup($principal[2], $event, $addressbookData, Addressbook::SUBJECT_SHARE_USER);
  236. $parameters = [
  237. 'actor' => $event->getAuthor(),
  238. 'addressbook' => [
  239. 'id' => (int)$addressbookData['id'],
  240. 'uri' => $addressbookData['uri'],
  241. 'name' => $addressbookData['{DAV:}displayname'],
  242. ],
  243. 'group' => $principal[2],
  244. ];
  245. if ($owner === $event->getAuthor()) {
  246. $subject = Addressbook::SUBJECT_SHARE_GROUP . '_you';
  247. } else {
  248. $event->setAffectedUser($event->getAuthor())
  249. ->setSubject(Addressbook::SUBJECT_SHARE_GROUP . '_you', $parameters);
  250. $this->activityManager->publish($event);
  251. $subject = Addressbook::SUBJECT_SHARE_GROUP . '_by';
  252. }
  253. $event->setAffectedUser($owner)
  254. ->setSubject($subject, $parameters);
  255. $this->activityManager->publish($event);
  256. }
  257. }
  258. }
  259. /**
  260. * Checks if a calendar is already shared with a principal
  261. *
  262. * @param string $principal
  263. * @param array[] $shares
  264. * @return bool
  265. */
  266. protected function isAlreadyShared(string $principal, array $shares): bool {
  267. foreach ($shares as $share) {
  268. if ($principal === $share['href']) {
  269. return true;
  270. }
  271. }
  272. return false;
  273. }
  274. /**
  275. * Creates the given activity for all members of the given group
  276. *
  277. * @param string $gid
  278. * @param IEvent $event
  279. * @param array $properties
  280. * @param string $subject
  281. */
  282. protected function triggerActivityGroup(string $gid, IEvent $event, array $properties, string $subject): void {
  283. $group = $this->groupManager->get($gid);
  284. if ($group instanceof IGroup) {
  285. foreach ($group->getUsers() as $user) {
  286. // Exclude current user
  287. if ($user->getUID() !== $event->getAuthor()) {
  288. $this->triggerActivityUser($user->getUID(), $event, $properties, $subject);
  289. }
  290. }
  291. }
  292. }
  293. /**
  294. * Creates the given activity for the given user
  295. *
  296. * @param string $user
  297. * @param IEvent $event
  298. * @param array $properties
  299. * @param string $subject
  300. * @param string $subjectSelf
  301. */
  302. protected function triggerActivityUser(string $user, IEvent $event, array $properties, string $subject, string $subjectSelf = ''): void {
  303. $event->setAffectedUser($user)
  304. ->setSubject(
  305. $user === $event->getAuthor() && $subjectSelf ? $subjectSelf : $subject,
  306. [
  307. 'actor' => $event->getAuthor(),
  308. 'addressbook' => [
  309. 'id' => (int)$properties['id'],
  310. 'uri' => $properties['uri'],
  311. 'name' => $properties['{DAV:}displayname'],
  312. ],
  313. ]
  314. );
  315. $this->activityManager->publish($event);
  316. }
  317. /**
  318. * Creates activities when a card was created/updated/deleted
  319. *
  320. * @param string $action
  321. * @param array $addressbookData
  322. * @param array $shares
  323. * @param array $cardData
  324. */
  325. public function triggerCardActivity(string $action, array $addressbookData, array $shares, array $cardData): void {
  326. if (!isset($addressbookData['principaluri'])) {
  327. return;
  328. }
  329. $principalUri = $addressbookData['principaluri'];
  330. // We are not interested in changes from the system addressbook
  331. if ($principalUri === 'principals/system/system') {
  332. return;
  333. }
  334. $principal = explode('/', $principalUri);
  335. $owner = array_pop($principal);
  336. $currentUser = $this->userSession->getUser();
  337. if ($currentUser instanceof IUser) {
  338. $currentUser = $currentUser->getUID();
  339. } else {
  340. $currentUser = $owner;
  341. }
  342. $card = $this->getCardNameAndId($cardData);
  343. $event = $this->activityManager->generateEvent();
  344. $event->setApp('dav')
  345. ->setObject('addressbook', (int)$addressbookData['id'])
  346. ->setType('contacts')
  347. ->setAuthor($currentUser);
  348. $users = $this->getUsersForShares($shares);
  349. $users[] = $owner;
  350. // Users for share can return the owner itself if the calendar is published
  351. foreach (array_unique($users) as $user) {
  352. $params = [
  353. 'actor' => $event->getAuthor(),
  354. 'addressbook' => [
  355. 'id' => (int)$addressbookData['id'],
  356. 'uri' => $addressbookData['uri'],
  357. 'name' => $addressbookData['{DAV:}displayname'],
  358. ],
  359. 'card' => [
  360. 'id' => $card['id'],
  361. 'name' => $card['name'],
  362. ],
  363. ];
  364. $event->setAffectedUser($user)
  365. ->setSubject(
  366. $user === $currentUser ? $action . '_self' : $action,
  367. $params
  368. );
  369. $this->activityManager->publish($event);
  370. }
  371. }
  372. /**
  373. * @param array $cardData
  374. * @return string[]
  375. */
  376. protected function getCardNameAndId(array $cardData): array {
  377. $vObject = Reader::read($cardData['carddata']);
  378. return ['id' => (string)$vObject->UID, 'name' => (string)($vObject->FN ?? '')];
  379. }
  380. /**
  381. * Get all users that have access to a given calendar
  382. *
  383. * @param array $shares
  384. * @return string[]
  385. */
  386. protected function getUsersForShares(array $shares): array {
  387. $users = $groups = [];
  388. foreach ($shares as $share) {
  389. $principal = explode('/', $share['{http://owncloud.org/ns}principal']);
  390. if ($principal[1] === 'users') {
  391. $users[] = $principal[2];
  392. } elseif ($principal[1] === 'groups') {
  393. $groups[] = $principal[2];
  394. }
  395. }
  396. if (!empty($groups)) {
  397. foreach ($groups as $gid) {
  398. $group = $this->groupManager->get($gid);
  399. if ($group instanceof IGroup) {
  400. foreach ($group->getUsers() as $user) {
  401. $users[] = $user->getUID();
  402. }
  403. }
  404. }
  405. }
  406. return array_unique($users);
  407. }
  408. }