Manager.php 12 KB

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