1
0

Manager.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  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\Calendar;
  8. use OC\AppFramework\Bootstrap\Coordinator;
  9. use OCP\AppFramework\Utility\ITimeFactory;
  10. use OCP\Calendar\Exceptions\CalendarException;
  11. use OCP\Calendar\ICalendar;
  12. use OCP\Calendar\ICalendarIsShared;
  13. use OCP\Calendar\ICalendarIsWritable;
  14. use OCP\Calendar\ICalendarProvider;
  15. use OCP\Calendar\ICalendarQuery;
  16. use OCP\Calendar\ICreateFromString;
  17. use OCP\Calendar\IHandleImipMessage;
  18. use OCP\Calendar\IManager;
  19. use Psr\Container\ContainerInterface;
  20. use Psr\Log\LoggerInterface;
  21. use Sabre\VObject\Component\VCalendar;
  22. use Sabre\VObject\Component\VEvent;
  23. use Sabre\VObject\Property\VCard\DateTime;
  24. use Sabre\VObject\Reader;
  25. use Throwable;
  26. use function array_map;
  27. use function array_merge;
  28. class Manager implements IManager {
  29. /**
  30. * @var ICalendar[] holds all registered calendars
  31. */
  32. private array $calendars = [];
  33. /**
  34. * @var \Closure[] to call to load/register calendar providers
  35. */
  36. private array $calendarLoaders = [];
  37. public function __construct(
  38. private Coordinator $coordinator,
  39. private ContainerInterface $container,
  40. private LoggerInterface $logger,
  41. private ITimeFactory $timeFactory,
  42. ) {
  43. }
  44. /**
  45. * This function is used to search and find objects within the user's calendars.
  46. * In case $pattern is empty all events/journals/todos will be returned.
  47. *
  48. * @param string $pattern which should match within the $searchProperties
  49. * @param array $searchProperties defines the properties within the query pattern should match
  50. * @param array $options - optional parameters:
  51. * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
  52. * @param integer|null $limit - limit number of search results
  53. * @param integer|null $offset - offset for paging of search results
  54. * @return array an array of events/journals/todos which are arrays of arrays of key-value-pairs
  55. * @since 13.0.0
  56. */
  57. public function search(
  58. $pattern,
  59. array $searchProperties = [],
  60. array $options = [],
  61. $limit = null,
  62. $offset = null,
  63. ): array {
  64. $this->loadCalendars();
  65. $result = [];
  66. foreach ($this->calendars as $calendar) {
  67. $r = $calendar->search($pattern, $searchProperties, $options, $limit, $offset);
  68. foreach ($r as $o) {
  69. $o['calendar-key'] = $calendar->getKey();
  70. $result[] = $o;
  71. }
  72. }
  73. return $result;
  74. }
  75. /**
  76. * Check if calendars are available
  77. *
  78. * @return bool true if enabled, false if not
  79. * @since 13.0.0
  80. */
  81. public function isEnabled(): bool {
  82. return !empty($this->calendars) || !empty($this->calendarLoaders);
  83. }
  84. /**
  85. * Registers a calendar
  86. *
  87. * @since 13.0.0
  88. */
  89. public function registerCalendar(ICalendar $calendar): void {
  90. $this->calendars[$calendar->getKey()] = $calendar;
  91. }
  92. /**
  93. * Unregisters a calendar
  94. *
  95. * @since 13.0.0
  96. */
  97. public function unregisterCalendar(ICalendar $calendar): void {
  98. unset($this->calendars[$calendar->getKey()]);
  99. }
  100. /**
  101. * In order to improve lazy loading a closure can be registered which will be called in case
  102. * calendars are actually requested
  103. *
  104. * @since 13.0.0
  105. */
  106. public function register(\Closure $callable): void {
  107. $this->calendarLoaders[] = $callable;
  108. }
  109. /**
  110. * @return ICalendar[]
  111. *
  112. * @since 13.0.0
  113. */
  114. public function getCalendars(): array {
  115. $this->loadCalendars();
  116. return array_values($this->calendars);
  117. }
  118. /**
  119. * removes all registered calendar instances
  120. *
  121. * @since 13.0.0
  122. */
  123. public function clear(): void {
  124. $this->calendars = [];
  125. $this->calendarLoaders = [];
  126. }
  127. /**
  128. * loads all calendars
  129. */
  130. private function loadCalendars(): void {
  131. foreach ($this->calendarLoaders as $callable) {
  132. $callable($this);
  133. }
  134. $this->calendarLoaders = [];
  135. }
  136. /**
  137. * @return ICreateFromString[]
  138. */
  139. public function getCalendarsForPrincipal(string $principalUri, array $calendarUris = []): array {
  140. $context = $this->coordinator->getRegistrationContext();
  141. if ($context === null) {
  142. return [];
  143. }
  144. return array_merge(
  145. ...array_map(function ($registration) use ($principalUri, $calendarUris) {
  146. try {
  147. /** @var ICalendarProvider $provider */
  148. $provider = $this->container->get($registration->getService());
  149. } catch (Throwable $e) {
  150. $this->logger->error('Could not load calendar provider ' . $registration->getService() . ': ' . $e->getMessage(), [
  151. 'exception' => $e,
  152. ]);
  153. return [];
  154. }
  155. return $provider->getCalendars($principalUri, $calendarUris);
  156. }, $context->getCalendarProviders())
  157. );
  158. }
  159. public function searchForPrincipal(ICalendarQuery $query): array {
  160. /** @var CalendarQuery $query */
  161. $calendars = $this->getCalendarsForPrincipal(
  162. $query->getPrincipalUri(),
  163. $query->getCalendarUris(),
  164. );
  165. $results = [];
  166. foreach ($calendars as $calendar) {
  167. $r = $calendar->search(
  168. $query->getSearchPattern() ?? '',
  169. $query->getSearchProperties(),
  170. $query->getOptions(),
  171. $query->getLimit(),
  172. $query->getOffset()
  173. );
  174. foreach ($r as $o) {
  175. $o['calendar-key'] = $calendar->getKey();
  176. $o['calendar-uri'] = $calendar->getUri();
  177. $results[] = $o;
  178. }
  179. }
  180. return $results;
  181. }
  182. public function newQuery(string $principalUri): ICalendarQuery {
  183. return new CalendarQuery($principalUri);
  184. }
  185. /**
  186. * @since 31.0.0
  187. * @throws \OCP\DB\Exception
  188. */
  189. public function handleIMipRequest(
  190. string $principalUri,
  191. string $sender,
  192. string $recipient,
  193. string $calendarData,
  194. ): bool {
  195. $userCalendars = $this->getCalendarsForPrincipal($principalUri);
  196. if (empty($userCalendars)) {
  197. $this->logger->warning('iMip message could not be processed because user has no calendars');
  198. return false;
  199. }
  200. /** @var VCalendar $vObject|null */
  201. $calendarObject = Reader::read($calendarData);
  202. if (!isset($calendarObject->METHOD) || $calendarObject->METHOD->getValue() !== 'REQUEST') {
  203. $this->logger->warning('iMip message contains an incorrect or invalid method');
  204. return false;
  205. }
  206. if (!isset($calendarObject->VEVENT)) {
  207. $this->logger->warning('iMip message contains no event');
  208. return false;
  209. }
  210. $eventObject = $calendarObject->VEVENT;
  211. if (!isset($eventObject->UID)) {
  212. $this->logger->warning('iMip message event dose not contains a UID');
  213. return false;
  214. }
  215. if (!isset($eventObject->ATTENDEE)) {
  216. $this->logger->warning('iMip message event dose not contains any attendees');
  217. return false;
  218. }
  219. foreach ($eventObject->ATTENDEE as $entry) {
  220. $address = trim(str_replace('mailto:', '', $entry->getValue()));
  221. if ($address === $recipient) {
  222. $attendee = $address;
  223. break;
  224. }
  225. }
  226. if (!isset($attendee)) {
  227. $this->logger->warning('iMip message event does not contain a attendee that matches the recipient');
  228. return false;
  229. }
  230. foreach ($userCalendars as $calendar) {
  231. if (!$calendar instanceof ICalendarIsWritable && !$calendar instanceof ICalendarIsShared) {
  232. continue;
  233. }
  234. if ($calendar->isDeleted() || !$calendar->isWritable() || $calendar->isShared()) {
  235. continue;
  236. }
  237. if (!empty($calendar->search($recipient, ['ATTENDEE'], ['uid' => $eventObject->UID->getValue()]))) {
  238. try {
  239. if ($calendar instanceof IHandleImipMessage) {
  240. $calendar->handleIMipMessage('', $calendarData);
  241. }
  242. return true;
  243. } catch (CalendarException $e) {
  244. $this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
  245. return false;
  246. }
  247. }
  248. }
  249. $this->logger->warning('iMip message event could not be processed because the no corresponding event was found in any calendar');
  250. return false;
  251. }
  252. /**
  253. * @throws \OCP\DB\Exception
  254. */
  255. public function handleIMipReply(
  256. string $principalUri,
  257. string $sender,
  258. string $recipient,
  259. string $calendarData,
  260. ): bool {
  261. /** @var VCalendar $vObject|null */
  262. $vObject = Reader::read($calendarData);
  263. if ($vObject === null) {
  264. return false;
  265. }
  266. /** @var VEvent|null $vEvent */
  267. $vEvent = $vObject->{'VEVENT'};
  268. if ($vEvent === null) {
  269. return false;
  270. }
  271. // First, we check if the correct method is passed to us
  272. if (strcasecmp('REPLY', $vObject->{'METHOD'}->getValue()) !== 0) {
  273. $this->logger->warning('Wrong method provided for processing');
  274. return false;
  275. }
  276. // check if mail recipient and organizer are one and the same
  277. $organizer = substr($vEvent->{'ORGANIZER'}->getValue(), 7);
  278. if (strcasecmp($recipient, $organizer) !== 0) {
  279. $this->logger->warning('Recipient and ORGANIZER must be identical');
  280. return false;
  281. }
  282. //check if the event is in the future
  283. /** @var DateTime $eventTime */
  284. $eventTime = $vEvent->{'DTSTART'};
  285. if ($eventTime->getDateTime()->getTimeStamp() < $this->timeFactory->getTime()) { // this might cause issues with recurrences
  286. $this->logger->warning('Only events in the future are processed');
  287. return false;
  288. }
  289. $calendars = $this->getCalendarsForPrincipal($principalUri);
  290. if (empty($calendars)) {
  291. $this->logger->warning('Could not find any calendars for principal ' . $principalUri);
  292. return false;
  293. }
  294. $found = null;
  295. // if the attendee has been found in at least one calendar event with the UID of the iMIP event
  296. // we process it.
  297. // Benefit: no attendee lost
  298. // Drawback: attendees that have been deleted will still be able to update their partstat
  299. foreach ($calendars as $calendar) {
  300. // We should not search in writable calendars
  301. if ($calendar instanceof IHandleImipMessage) {
  302. $o = $calendar->search($sender, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
  303. if (!empty($o)) {
  304. $found = $calendar;
  305. $name = $o[0]['uri'];
  306. break;
  307. }
  308. }
  309. }
  310. if (empty($found)) {
  311. $this->logger->info('Event not found in any calendar for principal ' . $principalUri . 'and UID' . $vEvent->{'UID'}->getValue());
  312. return false;
  313. }
  314. try {
  315. $found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
  316. } catch (CalendarException $e) {
  317. $this->logger->error('Could not update calendar for iMIP processing', ['exception' => $e]);
  318. return false;
  319. }
  320. return true;
  321. }
  322. /**
  323. * @since 25.0.0
  324. * @throws \OCP\DB\Exception
  325. */
  326. public function handleIMipCancel(
  327. string $principalUri,
  328. string $sender,
  329. ?string $replyTo,
  330. string $recipient,
  331. string $calendarData,
  332. ): bool {
  333. /** @var VCalendar $vObject|null */
  334. $vObject = Reader::read($calendarData);
  335. if ($vObject === null) {
  336. return false;
  337. }
  338. /** @var VEvent|null $vEvent */
  339. $vEvent = $vObject->{'VEVENT'};
  340. if ($vEvent === null) {
  341. return false;
  342. }
  343. // First, we check if the correct method is passed to us
  344. if (strcasecmp('CANCEL', $vObject->{'METHOD'}->getValue()) !== 0) {
  345. $this->logger->warning('Wrong method provided for processing');
  346. return false;
  347. }
  348. $attendee = substr($vEvent->{'ATTENDEE'}->getValue(), 7);
  349. if (strcasecmp($recipient, $attendee) !== 0) {
  350. $this->logger->warning('Recipient must be an ATTENDEE of this event');
  351. return false;
  352. }
  353. // Thirdly, we need to compare the email address the CANCEL is coming from (in Mail)
  354. // or the Reply- To Address submitted with the CANCEL email
  355. // to the email address in the ORGANIZER.
  356. // We don't want to accept a CANCEL request from just anyone
  357. $organizer = substr($vEvent->{'ORGANIZER'}->getValue(), 7);
  358. $isNotOrganizer = ($replyTo !== null) ? (strcasecmp($sender, $organizer) !== 0 && strcasecmp($replyTo, $organizer) !== 0) : (strcasecmp($sender, $organizer) !== 0);
  359. if ($isNotOrganizer) {
  360. $this->logger->warning('Sender must be the ORGANIZER of this event');
  361. return false;
  362. }
  363. //check if the event is in the future
  364. /** @var DateTime $eventTime */
  365. $eventTime = $vEvent->{'DTSTART'};
  366. if ($eventTime->getDateTime()->getTimeStamp() < $this->timeFactory->getTime()) { // this might cause issues with recurrences
  367. $this->logger->warning('Only events in the future are processed');
  368. return false;
  369. }
  370. // Check if we have a calendar to work with
  371. $calendars = $this->getCalendarsForPrincipal($principalUri);
  372. if (empty($calendars)) {
  373. $this->logger->warning('Could not find any calendars for principal ' . $principalUri);
  374. return false;
  375. }
  376. $found = null;
  377. // if the attendee has been found in at least one calendar event with the UID of the iMIP event
  378. // we process it.
  379. // Benefit: no attendee lost
  380. // Drawback: attendees that have been deleted will still be able to update their partstat
  381. foreach ($calendars as $calendar) {
  382. // We should not search in writable calendars
  383. if ($calendar instanceof IHandleImipMessage) {
  384. $o = $calendar->search($recipient, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
  385. if (!empty($o)) {
  386. $found = $calendar;
  387. $name = $o[0]['uri'];
  388. break;
  389. }
  390. }
  391. }
  392. if (empty($found)) {
  393. $this->logger->info('Event not found in any calendar for principal ' . $principalUri . 'and UID' . $vEvent->{'UID'}->getValue());
  394. // this is a safe operation
  395. // we can ignore events that have been cancelled but were not in the calendar anyway
  396. return true;
  397. }
  398. try {
  399. $found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
  400. return true;
  401. } catch (CalendarException $e) {
  402. $this->logger->error('Could not update calendar for iMIP processing', ['exception' => $e]);
  403. return false;
  404. }
  405. }
  406. }