1
0

Manager.php 13 KB

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