Manager.php 11 KB

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