diff options
Diffstat (limited to 'apps/dav/lib/CalDAV/Reminder')
11 files changed, 401 insertions, 612 deletions
diff --git a/apps/dav/lib/CalDAV/Reminder/Backend.php b/apps/dav/lib/CalDAV/Reminder/Backend.php index b0476e9594c..329af3a2f56 100644 --- a/apps/dav/lib/CalDAV/Reminder/Backend.php +++ b/apps/dav/lib/CalDAV/Reminder/Backend.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; @@ -38,22 +18,16 @@ use OCP\IDBConnection; */ class Backend { - /** @var IDBConnection */ - protected $db; - - /** @var ITimeFactory */ - private $timeFactory; - /** * Backend constructor. * * @param IDBConnection $db * @param ITimeFactory $timeFactory */ - public function __construct(IDBConnection $db, - ITimeFactory $timeFactory) { - $this->db = $db; - $this->timeFactory = $timeFactory; + public function __construct( + protected IDBConnection $db, + protected ITimeFactory $timeFactory, + ) { } /** @@ -64,12 +38,13 @@ class Backend { */ public function getRemindersToProcess():array { $query = $this->db->getQueryBuilder(); - $query->select(['cr.*', 'co.calendardata', 'c.displayname', 'c.principaluri']) + $query->select(['cr.id', 'cr.calendar_id','cr.object_id','cr.is_recurring','cr.uid','cr.recurrence_id','cr.is_recurrence_exception','cr.event_hash','cr.alarm_hash','cr.type','cr.is_relative','cr.notification_date','cr.is_repeat_based','co.calendardata', 'c.displayname', 'c.principaluri']) ->from('calendar_reminders', 'cr') ->where($query->expr()->lte('cr.notification_date', $query->createNamedParameter($this->timeFactory->getTime()))) ->join('cr', 'calendarobjects', 'co', $query->expr()->eq('cr.object_id', 'co.id')) - ->join('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id')); - $stmt = $query->execute(); + ->join('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id')) + ->groupBy('cr.event_hash', 'cr.notification_date', 'cr.type', 'cr.id', 'cr.calendar_id', 'cr.object_id', 'cr.is_recurring', 'cr.uid', 'cr.recurrence_id', 'cr.is_recurrence_exception', 'cr.alarm_hash', 'cr.is_relative', 'cr.is_repeat_based', 'co.calendardata', 'c.displayname', 'c.principaluri'); + $stmt = $query->executeQuery(); return array_map( [$this, 'fixRowTyping'], @@ -88,7 +63,7 @@ class Backend { $query->select('*') ->from('calendar_reminders') ->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId))); - $stmt = $query->execute(); + $stmt = $query->executeQuery(); return array_map( [$this, 'fixRowTyping'], @@ -114,17 +89,17 @@ class Backend { * @return int The insert id */ public function insertReminder(int $calendarId, - int $objectId, - string $uid, - bool $isRecurring, - int $recurrenceId, - bool $isRecurrenceException, - string $eventHash, - string $alarmHash, - string $type, - bool $isRelative, - int $notificationDate, - bool $isRepeatBased):int { + int $objectId, + string $uid, + bool $isRecurring, + int $recurrenceId, + bool $isRecurrenceException, + string $eventHash, + string $alarmHash, + string $type, + bool $isRelative, + int $notificationDate, + bool $isRepeatBased):int { $query = $this->db->getQueryBuilder(); $query->insert('calendar_reminders') ->values([ @@ -141,7 +116,7 @@ class Backend { 'notification_date' => $query->createNamedParameter($notificationDate), 'is_repeat_based' => $query->createNamedParameter($isRepeatBased ? 1 : 0), ]) - ->execute(); + ->executeStatement(); return $query->getLastInsertId(); } @@ -153,12 +128,12 @@ class Backend { * @param int $newNotificationDate */ public function updateReminder(int $reminderId, - int $newNotificationDate):void { + int $newNotificationDate):void { $query = $this->db->getQueryBuilder(); $query->update('calendar_reminders') ->set('notification_date', $query->createNamedParameter($newNotificationDate)) ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) - ->execute(); + ->executeStatement(); } /** @@ -172,7 +147,7 @@ class Backend { $query->delete('calendar_reminders') ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) - ->execute(); + ->executeStatement(); } /** @@ -185,7 +160,7 @@ class Backend { $query->delete('calendar_reminders') ->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId))) - ->execute(); + ->executeStatement(); } /** @@ -199,7 +174,7 @@ class Backend { $query->delete('calendar_reminders') ->where($query->expr()->eq('calendar_id', $query->createNamedParameter($calendarId))) - ->execute(); + ->executeStatement(); } /** @@ -207,15 +182,15 @@ class Backend { * @return array */ private function fixRowTyping(array $row): array { - $row['id'] = (int) $row['id']; - $row['calendar_id'] = (int) $row['calendar_id']; - $row['object_id'] = (int) $row['object_id']; - $row['is_recurring'] = (bool) $row['is_recurring']; - $row['recurrence_id'] = (int) $row['recurrence_id']; - $row['is_recurrence_exception'] = (bool) $row['is_recurrence_exception']; - $row['is_relative'] = (bool) $row['is_relative']; - $row['notification_date'] = (int) $row['notification_date']; - $row['is_repeat_based'] = (bool) $row['is_repeat_based']; + $row['id'] = (int)$row['id']; + $row['calendar_id'] = (int)$row['calendar_id']; + $row['object_id'] = (int)$row['object_id']; + $row['is_recurring'] = (bool)$row['is_recurring']; + $row['recurrence_id'] = (int)$row['recurrence_id']; + $row['is_recurrence_exception'] = (bool)$row['is_recurrence_exception']; + $row['is_relative'] = (bool)$row['is_relative']; + $row['notification_date'] = (int)$row['notification_date']; + $row['is_repeat_based'] = (bool)$row['is_repeat_based']; return $row; } diff --git a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php index a6b439c0b4f..31d60f1531d 100644 --- a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; @@ -41,11 +22,13 @@ interface INotificationProvider { * Send notification * * @param VEvent $vevent - * @param string $calendarDisplayName + * @param string|null $calendarDisplayName + * @param string[] $principalEmailAddresses All email addresses associated to the principal owning the calendar object * @param IUser[] $users * @return void */ public function send(VEvent $vevent, - string $calendarDisplayName, - array $users = []): void; + ?string $calendarDisplayName, + array $principalEmailAddresses, + array $users = []): void; } diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php index 044e5fac4e2..94edff98e52 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php @@ -3,39 +3,18 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; use OCA\DAV\CalDAV\Reminder\INotificationProvider; use OCP\IConfig; use OCP\IL10N; -use OCP\ILogger; use OCP\IURLGenerator; use OCP\IUser; use OCP\L10N\IFactory as L10NFactory; +use Psr\Log\LoggerInterface; use Sabre\VObject\Component\VEvent; use Sabre\VObject\DateTimeParser; use Sabre\VObject\Property; @@ -50,51 +29,33 @@ abstract class AbstractProvider implements INotificationProvider { /** @var string */ public const NOTIFICATION_TYPE = ''; - /** @var ILogger */ - protected $logger; - - /** @var L10NFactory */ - protected $l10nFactory; - /** @var IL10N[] */ private $l10ns; /** @var string */ private $fallbackLanguage; - /** @var IURLGenerator */ - protected $urlGenerator; - - /** @var IConfig */ - protected $config; - - /** - * @param ILogger $logger - * @param L10NFactory $l10nFactory - * @param IConfig $config - * @param IUrlGenerator $urlGenerator - */ - public function __construct(ILogger $logger, - L10NFactory $l10nFactory, - IURLGenerator $urlGenerator, - IConfig $config) { - $this->logger = $logger; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; - $this->config = $config; + public function __construct( + protected LoggerInterface $logger, + protected L10NFactory $l10nFactory, + protected IURLGenerator $urlGenerator, + protected IConfig $config, + ) { } /** * Send notification * * @param VEvent $vevent - * @param string $calendarDisplayName + * @param string|null $calendarDisplayName + * @param string[] $principalEmailAddresses * @param IUser[] $users * @return void */ abstract public function send(VEvent $vevent, - string $calendarDisplayName, - array $users = []): void; + ?string $calendarDisplayName, + array $principalEmailAddresses, + array $users = []): void; /** * @return string @@ -139,7 +100,7 @@ abstract class AbstractProvider implements INotificationProvider { */ private function getStatusOfEvent(VEvent $vevent):string { if ($vevent->STATUS) { - return (string) $vevent->STATUS; + return (string)$vevent->STATUS; } // Doesn't say so in the standard, @@ -189,4 +150,8 @@ abstract class AbstractProvider implements INotificationProvider { return clone $vevent->DTSTART; } + + protected function getCalendarDisplayNameFallback(string $lang): string { + return $this->getL10NForLang($lang)->t('Untitled calendar'); + } } diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php index 4b369b34dc0..01d51489a3b 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php index 456b9f8b42d..0fd39a9e459 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php @@ -3,42 +3,22 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; use DateTime; use OCP\IConfig; use OCP\IL10N; -use OCP\ILogger; use OCP\IURLGenerator; +use OCP\IUser; use OCP\L10N\IFactory as L10NFactory; +use OCP\Mail\Headers\AutoSubmitted; use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; +use OCP\Util; +use Psr\Log\LoggerInterface; use Sabre\VObject; use Sabre\VObject\Component\VEvent; use Sabre\VObject\Parameter; @@ -50,44 +30,46 @@ use Sabre\VObject\Property; * @package OCA\DAV\CalDAV\Reminder\NotificationProvider */ class EmailProvider extends AbstractProvider { - /** @var string */ public const NOTIFICATION_TYPE = 'EMAIL'; - /** @var IMailer */ - private $mailer; - - /** - * @param IConfig $config - * @param IMailer $mailer - * @param ILogger $logger - * @param L10NFactory $l10nFactory - * @param IUrlGenerator $urlGenerator - */ - public function __construct(IConfig $config, - IMailer $mailer, - ILogger $logger, - L10NFactory $l10nFactory, - IURLGenerator $urlGenerator) { + public function __construct( + IConfig $config, + private IMailer $mailer, + LoggerInterface $logger, + L10NFactory $l10nFactory, + IURLGenerator $urlGenerator, + ) { parent::__construct($logger, $l10nFactory, $urlGenerator, $config); - $this->mailer = $mailer; } /** * Send out notification via email * * @param VEvent $vevent - * @param string $calendarDisplayName + * @param string|null $calendarDisplayName + * @param string[] $principalEmailAddresses * @param array $users * @throws \Exception */ public function send(VEvent $vevent, - string $calendarDisplayName, - array $users = []):void { + ?string $calendarDisplayName, + array $principalEmailAddresses, + array $users = []):void { $fallbackLanguage = $this->getFallbackLanguage(); + $organizerEmailAddress = null; + if (isset($vevent->ORGANIZER)) { + $organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER); + } + $emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users); - $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent); + $emailAddressesOfAttendees = []; + if (count($principalEmailAddresses) === 0 + || ($organizerEmailAddress && in_array($organizerEmailAddress, $principalEmailAddresses, true)) + ) { + $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent); + } // Quote from php.net: // If the input arrays have the same string keys, then the later value for that key will overwrite the previous one. @@ -105,12 +87,12 @@ class EmailProvider extends AbstractProvider { $lang = $fallbackLanguage; } $l10n = $this->getL10NForLang($lang); - $fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply'); + $fromEMail = Util::getDefaultEmailAddress('reminders-noreply'); $template = $this->mailer->createEMailTemplate('dav.calendarReminder'); $template->addHeader(); $this->addSubjectAndHeading($template, $l10n, $vevent); - $this->addBulletList($template, $l10n, $calendarDisplayName, $vevent); + $this->addBulletList($template, $l10n, $calendarDisplayName ?? $this->getCalendarDisplayNameFallback($lang), $vevent); $template->addFooter(); foreach ($emailAddresses as $emailAddress) { @@ -126,6 +108,7 @@ class EmailProvider extends AbstractProvider { } $message->setTo([$emailAddress]); $message->useTemplate($template); + $message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED); try { $failed = $this->mailer->send($message); @@ -133,7 +116,7 @@ class EmailProvider extends AbstractProvider { $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]); } } catch (\Exception $ex) { - $this->logger->logException($ex, ['app' => 'dav']); + $this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]); } } } @@ -156,9 +139,9 @@ class EmailProvider extends AbstractProvider { * @param array $eventData */ private function addBulletList(IEMailTemplate $template, - IL10N $l10n, - string $calendarDisplayName, - VEvent $vevent):void { + IL10N $l10n, + string $calendarDisplayName, + VEvent $vevent):void { $template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'), $this->getAbsoluteImagePath('actions/info.png')); @@ -166,19 +149,15 @@ class EmailProvider extends AbstractProvider { $this->getAbsoluteImagePath('places/calendar.png')); if (isset($vevent->LOCATION)) { - $template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'), + $template->addBodyListItem((string)$vevent->LOCATION, $l10n->t('Where:'), $this->getAbsoluteImagePath('actions/address.png')); } if (isset($vevent->DESCRIPTION)) { - $template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'), + $template->addBodyListItem((string)$vevent->DESCRIPTION, $l10n->t('Description:'), $this->getAbsoluteImagePath('actions/more.png')); } } - /** - * @param string $path - * @return string - */ private function getAbsoluteImagePath(string $path):string { return $this->urlGenerator->getAbsoluteURL( $this->urlGenerator->imagePath('core', $path) @@ -201,7 +180,7 @@ class EmailProvider extends AbstractProvider { $organizerEMail = substr($organizer->getValue(), 7); - if ($organizerEMail === false || !$this->mailer->validateMailAddress($organizerEMail)) { + if (!$this->mailer->validateMailAddress($organizerEMail)) { return null; } @@ -214,12 +193,11 @@ class EmailProvider extends AbstractProvider { } /** - * @param array $emails - * @param string $defaultLanguage - * @return array + * @param array<string, array{LANG?: string}> $emails + * @return array<string, string[]> */ private function sortEMailAddressesByLanguage(array $emails, - string $defaultLanguage):array { + string $defaultLanguage):array { $sortedByLanguage = []; foreach ($emails as $emailAddress => $parameters) { @@ -241,7 +219,7 @@ class EmailProvider extends AbstractProvider { /** * @param VEvent $vevent - * @return array + * @return array<string, array{LANG?: string}> */ private function getAllEMailAddressesFromEvent(VEvent $vevent):array { $emailAddresses = []; @@ -272,7 +250,10 @@ class EmailProvider extends AbstractProvider { $emailAddressesOfDelegates = $delegates->getParts(); foreach ($emailAddressesOfDelegates as $addressesOfDelegate) { if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) { - $emailAddresses[substr($addressesOfDelegate, 7)] = []; + $delegateEmail = substr($addressesOfDelegate, 7); + if ($this->mailer->validateMailAddress($delegateEmail)) { + $emailAddresses[$delegateEmail] = []; + } } } @@ -284,7 +265,7 @@ class EmailProvider extends AbstractProvider { $properties = []; $langProp = $attendee->offsetGet('LANG'); - if ($langProp instanceof VObject\Parameter) { + if ($langProp instanceof VObject\Parameter && $langProp->getValue() !== null) { $properties['LANG'] = $langProp->getValue(); } @@ -294,18 +275,15 @@ class EmailProvider extends AbstractProvider { } if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) { - $emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = []; + $organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER); + if ($organizerEmailAddress !== null) { + $emailAddresses[$organizerEmailAddress] = []; + } } return $emailAddresses; } - - - /** - * @param VObject\Property $attendee - * @return string - */ private function getCUTypeOfAttendee(VObject\Property $attendee):string { $cuType = $attendee->offsetGet('CUTYPE'); if ($cuType instanceof VObject\Parameter) { @@ -315,10 +293,6 @@ class EmailProvider extends AbstractProvider { return 'INDIVIDUAL'; } - /** - * @param VObject\Property $attendee - * @return string - */ private function getPartstatOfAttendee(VObject\Property $attendee):string { $partstat = $attendee->offsetGet('PARTSTAT'); if ($partstat instanceof VObject\Parameter) { @@ -328,29 +302,25 @@ class EmailProvider extends AbstractProvider { return 'NEEDS-ACTION'; } - /** - * @param VObject\Property $attendee - * @return bool - */ - private function hasAttendeeMailURI(VObject\Property $attendee):bool { + private function hasAttendeeMailURI(VObject\Property $attendee): bool { return stripos($attendee->getValue(), 'mailto:') === 0; } - /** - * @param VObject\Property $attendee - * @return string|null - */ - private function getEMailAddressOfAttendee(VObject\Property $attendee):?string { + private function getEMailAddressOfAttendee(VObject\Property $attendee): ?string { if (!$this->hasAttendeeMailURI($attendee)) { return null; } + $attendeeEMail = substr($attendee->getValue(), 7); + if (!$this->mailer->validateMailAddress($attendeeEMail)) { + return null; + } - return substr($attendee->getValue(), 7); + return $attendeeEMail; } /** - * @param array $users - * @return array + * @param IUser[] $users + * @return array<string, array{LANG?: string}> */ private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array { $emailAddresses = []; @@ -373,12 +343,9 @@ class EmailProvider extends AbstractProvider { } /** - * @param IL10N $l10n - * @param VEvent $vevent - * @return string * @throws \Exception */ - private function generateDateString(IL10N $l10n, VEvent $vevent):string { + private function generateDateString(IL10N $l10n, VEvent $vevent): string { $isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date; /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */ @@ -444,57 +411,27 @@ class EmailProvider extends AbstractProvider { . ' (' . $startTimezone . ')'; } - /** - * @param DateTime $dtStart - * @param DateTime $dtEnd - * @return bool - */ private function isDayEqual(DateTime $dtStart, - DateTime $dtEnd):bool { + DateTime $dtEnd):bool { return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d'); } - /** - * @param IL10N $l10n - * @param DateTime $dt - * @return string - */ private function getWeekDayName(IL10N $l10n, DateTime $dt):string { - return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']); + return (string)$l10n->l('weekdayName', $dt, ['width' => 'abbreviated']); } - /** - * @param IL10N $l10n - * @param DateTime $dt - * @return string - */ private function getDateString(IL10N $l10n, DateTime $dt):string { - return $l10n->l('date', $dt, ['width' => 'medium']); + return (string)$l10n->l('date', $dt, ['width' => 'medium']); } - /** - * @param IL10N $l10n - * @param DateTime $dt - * @return string - */ private function getDateTimeString(IL10N $l10n, DateTime $dt):string { - return $l10n->l('datetime', $dt, ['width' => 'medium|short']); + return (string)$l10n->l('datetime', $dt, ['width' => 'medium|short']); } - /** - * @param IL10N $l10n - * @param DateTime $dt - * @return string - */ private function getTimeString(IL10N $l10n, DateTime $dt):string { - return $l10n->l('time', $dt, ['width' => 'short']); + return (string)$l10n->l('time', $dt, ['width' => 'short']); } - /** - * @param VEvent $vevent - * @param IL10N $l10n - * @return string - */ private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string { if (isset($vevent->SUMMARY)) { return (string)$vevent->SUMMARY; diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php index 2e4f9a38493..15994bacf49 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Thomas Citharel <tcit@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php index fb123960df8..a3f0cce547a 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php @@ -3,41 +3,20 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; use OCA\DAV\AppInfo\Application; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; -use OCP\ILogger; use OCP\IURLGenerator; use OCP\IUser; use OCP\L10N\IFactory as L10NFactory; use OCP\Notification\IManager; use OCP\Notification\INotification; +use Psr\Log\LoggerInterface; use Sabre\VObject\Component\VEvent; use Sabre\VObject\Property; @@ -51,55 +30,44 @@ class PushProvider extends AbstractProvider { /** @var string */ public const NOTIFICATION_TYPE = 'DISPLAY'; - /** @var IManager */ - private $manager; - - /** @var ITimeFactory */ - private $timeFactory; - - /** - * @param IConfig $config - * @param IManager $manager - * @param ILogger $logger - * @param L10NFactory $l10nFactory - * @param IUrlGenerator $urlGenerator - * @param ITimeFactory $timeFactory - */ - public function __construct(IConfig $config, - IManager $manager, - ILogger $logger, - L10NFactory $l10nFactory, - IURLGenerator $urlGenerator, - ITimeFactory $timeFactory) { + public function __construct( + IConfig $config, + private IManager $manager, + LoggerInterface $logger, + L10NFactory $l10nFactory, + IURLGenerator $urlGenerator, + private ITimeFactory $timeFactory, + ) { parent::__construct($logger, $l10nFactory, $urlGenerator, $config); - $this->manager = $manager; - $this->timeFactory = $timeFactory; } /** * Send push notification to all users. * * @param VEvent $vevent - * @param string $calendarDisplayName + * @param string|null $calendarDisplayName + * @param string[] $principalEmailAddresses * @param IUser[] $users * @throws \Exception */ public function send(VEvent $vevent, - string $calendarDisplayName = null, - array $users = []):void { - if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'no') !== 'yes') { + ?string $calendarDisplayName, + array $principalEmailAddresses, + array $users = []):void { + if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'yes') !== 'yes') { return; } $eventDetails = $this->extractEventDetails($vevent); - $eventDetails['calendar_displayname'] = $calendarDisplayName; - $eventUUID = (string) $vevent->UID; + $eventUUID = (string)$vevent->UID; if (!$eventUUID) { return; }; $eventUUIDHash = hash('sha256', $eventUUID, false); foreach ($users as $user) { + $eventDetails['calendar_displayname'] = $calendarDisplayName ?? $this->getCalendarDisplayNameFallback($this->l10nFactory->getUserLanguage($user)); + /** @var INotification $notification */ $notification = $this->manager->createNotification(); $notification->setApp(Application::APP_ID) @@ -117,8 +85,6 @@ class PushProvider extends AbstractProvider { } /** - * @var VEvent $vevent - * @return array * @throws \Exception */ protected function extractEventDetails(VEvent $vevent):array { @@ -128,13 +94,13 @@ class PushProvider extends AbstractProvider { return [ 'title' => isset($vevent->SUMMARY) - ? ((string) $vevent->SUMMARY) + ? ((string)$vevent->SUMMARY) : null, 'description' => isset($vevent->DESCRIPTION) - ? ((string) $vevent->DESCRIPTION) + ? ((string)$vevent->DESCRIPTION) : null, 'location' => isset($vevent->LOCATION) - ? ((string) $vevent->LOCATION) + ? ((string)$vevent->LOCATION) : null, 'all_day' => $start instanceof Property\ICalendar\Date, 'start_atom' => $start->getDateTime()->format(\DateTimeInterface::ATOM), diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php index cd8030a1177..265db09b061 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php @@ -3,30 +3,15 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException; +use OCP\AppFramework\QueryException; +use OCP\Server; + /** * Class NotificationProviderManager * @@ -61,7 +46,7 @@ class NotificationProviderManager { if (isset($this->providers[$type])) { return $this->providers[$type]; } - throw new NotificationProvider\ProviderNotAvailableException($type); + throw new ProviderNotAvailableException($type); } throw new NotificationTypeDoesNotExistException($type); } @@ -70,10 +55,10 @@ class NotificationProviderManager { * Registers a new provider * * @param string $providerClassName - * @throws \OCP\AppFramework\QueryException + * @throws QueryException */ public function registerProvider(string $providerClassName):void { - $provider = \OC::$server->query($providerClassName); + $provider = Server::get($providerClassName); if (!$provider instanceof INotificationProvider) { throw new \InvalidArgumentException('Invalid notification provider registered'); diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php index 16fb858bc3a..6fd2a29ede5 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; diff --git a/apps/dav/lib/CalDAV/Reminder/Notifier.php b/apps/dav/lib/CalDAV/Reminder/Notifier.php index 8535c55054a..137fb509f56 100644 --- a/apps/dav/lib/CalDAV/Reminder/Notifier.php +++ b/apps/dav/lib/CalDAV/Reminder/Notifier.php @@ -3,29 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; @@ -38,6 +17,7 @@ use OCP\L10N\IFactory; use OCP\Notification\AlreadyProcessedException; use OCP\Notification\INotification; use OCP\Notification\INotifier; +use OCP\Notification\UnknownNotificationException; /** * Class Notifier @@ -46,31 +26,21 @@ use OCP\Notification\INotifier; */ class Notifier implements INotifier { - /** @var IFactory */ - private $l10nFactory; - - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IL10N */ private $l10n; - /** @var ITimeFactory */ - private $timeFactory; - /** * Notifier constructor. * - * @param IFactory $factory + * @param IFactory $l10nFactory * @param IURLGenerator $urlGenerator * @param ITimeFactory $timeFactory */ - public function __construct(IFactory $factory, - IURLGenerator $urlGenerator, - ITimeFactory $timeFactory) { - $this->l10nFactory = $factory; - $this->urlGenerator = $urlGenerator; - $this->timeFactory = $timeFactory; + public function __construct( + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, + private ITimeFactory $timeFactory, + ) { } /** @@ -99,12 +69,12 @@ class Notifier implements INotifier { * @param INotification $notification * @param string $languageCode The code of the language that should be used to prepare the notification * @return INotification - * @throws \Exception + * @throws UnknownNotificationException */ public function prepare(INotification $notification, - string $languageCode):INotification { + string $languageCode):INotification { if ($notification->getApp() !== Application::APP_ID) { - throw new \InvalidArgumentException('Notification not from this app'); + throw new UnknownNotificationException('Notification not from this app'); } // Read the language from the notification @@ -116,7 +86,7 @@ class Notifier implements INotifier { return $this->prepareReminderNotification($notification); default: - throw new \InvalidArgumentException('Unknown subject'); + throw new UnknownNotificationException('Unknown subject'); } } @@ -170,21 +140,35 @@ class Notifier implements INotifier { $components[] = $this->l10n->n('%n minute', '%n minutes', $diff->i); } - // Limiting to the first three components to prevent - // the string from getting too long - $firstThreeComponents = array_slice($components, 0, 2); - $diffLabel = implode(', ', $firstThreeComponents); + if (count($components) > 0 && !$this->hasPhpDatetimeDiffBug()) { + // Limiting to the first three components to prevent + // the string from getting too long + $firstThreeComponents = array_slice($components, 0, 2); + $diffLabel = implode(', ', $firstThreeComponents); - if ($diff->invert) { - $title = $this->l10n->t('%s (in %s)', [$title, $diffLabel]); - } else { - $title = $this->l10n->t('%s (%s ago)', [$title, $diffLabel]); + if ($diff->invert) { + $title = $this->l10n->t('%s (in %s)', [$title, $diffLabel]); + } else { + $title = $this->l10n->t('%s (%s ago)', [$title, $diffLabel]); + } } $notification->setParsedSubject($title); } /** + * @see https://github.com/nextcloud/server/issues/41615 + * @see https://github.com/php/php-src/issues/9699 + */ + private function hasPhpDatetimeDiffBug(): bool { + $d1 = DateTime::createFromFormat(\DateTimeInterface::ATOM, '2023-11-22T11:52:00+01:00'); + $d2 = new DateTime('2023-11-22T10:52:03', new \DateTimeZone('UTC')); + + // The difference is 3 seconds, not -1year+11months+… + return $d1->diff($d2)->y < 0; + } + + /** * Sets the notification message based on the parameters set in PushProvider * * @param INotification $notification @@ -289,7 +273,7 @@ class Notifier implements INotifier { * @return bool */ private function isDayEqual(DateTime $dtStart, - DateTime $dtEnd):bool { + DateTime $dtEnd):bool { return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d'); } @@ -298,7 +282,7 @@ class Notifier implements INotifier { * @return string */ private function getWeekDayName(DateTime $dt):string { - return $this->l10n->l('weekdayName', $dt, ['width' => 'abbreviated']); + return (string)$this->l10n->l('weekdayName', $dt, ['width' => 'abbreviated']); } /** @@ -306,7 +290,7 @@ class Notifier implements INotifier { * @return string */ private function getDateString(DateTime $dt):string { - return $this->l10n->l('date', $dt, ['width' => 'medium']); + return (string)$this->l10n->l('date', $dt, ['width' => 'medium']); } /** @@ -314,7 +298,7 @@ class Notifier implements INotifier { * @return string */ private function getDateTimeString(DateTime $dt):string { - return $this->l10n->l('datetime', $dt, ['width' => 'medium|short']); + return (string)$this->l10n->l('datetime', $dt, ['width' => 'medium|short']); } /** @@ -322,6 +306,6 @@ class Notifier implements INotifier { * @return string */ private function getTimeString(DateTime $dt):string { - return $this->l10n->l('time', $dt, ['width' => 'short']); + return (string)$this->l10n->l('time', $dt, ['width' => 'short']); } } diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php index d6901cc4fb0..c75090e1560 100644 --- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php +++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php @@ -3,73 +3,35 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; use DateTimeImmutable; +use DateTimeZone; use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Connector\Sabre\Principal; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\IGroup; use OCP\IGroupManager; use OCP\IUser; use OCP\IUserManager; +use Psr\Log\LoggerInterface; use Sabre\VObject; use Sabre\VObject\Component\VAlarm; use Sabre\VObject\Component\VEvent; use Sabre\VObject\InvalidDataException; use Sabre\VObject\ParseException; use Sabre\VObject\Recur\EventIterator; +use Sabre\VObject\Recur\MaxInstancesExceededException; use Sabre\VObject\Recur\NoInstancesException; +use function count; use function strcasecmp; class ReminderService { - /** @var Backend */ - private $backend; - - /** @var NotificationProviderManager */ - private $notificationProviderManager; - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var CalDavBackend */ - private $caldavBackend; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var IConfig */ - private $config; - public const REMINDER_TYPE_EMAIL = 'EMAIL'; public const REMINDER_TYPE_DISPLAY = 'DISPLAY'; public const REMINDER_TYPE_AUDIO = 'AUDIO'; @@ -85,31 +47,17 @@ class ReminderService { self::REMINDER_TYPE_AUDIO ]; - /** - * ReminderService constructor. - * - * @param Backend $backend - * @param NotificationProviderManager $notificationProviderManager - * @param IUserManager $userManager - * @param IGroupManager $groupManager - * @param CalDavBackend $caldavBackend - * @param ITimeFactory $timeFactory - * @param IConfig $config - */ - public function __construct(Backend $backend, - NotificationProviderManager $notificationProviderManager, - IUserManager $userManager, - IGroupManager $groupManager, - CalDavBackend $caldavBackend, - ITimeFactory $timeFactory, - IConfig $config) { - $this->backend = $backend; - $this->notificationProviderManager = $notificationProviderManager; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->caldavBackend = $caldavBackend; - $this->timeFactory = $timeFactory; - $this->config = $config; + public function __construct( + private Backend $backend, + private NotificationProviderManager $notificationProviderManager, + private IUserManager $userManager, + private IGroupManager $groupManager, + private CalDavBackend $caldavBackend, + private ITimeFactory $timeFactory, + private IConfig $config, + private LoggerInterface $logger, + private Principal $principalConnector, + ) { } /** @@ -118,8 +66,11 @@ class ReminderService { * @throws NotificationProvider\ProviderNotAvailableException * @throws NotificationTypeDoesNotExistException */ - public function processReminders():void { + public function processReminders() :void { $reminders = $this->backend->getRemindersToProcess(); + $this->logger->debug('{numReminders} reminders to process', [ + 'numReminders' => count($reminders), + ]); foreach ($reminders as $reminder) { $calendarData = is_resource($reminder['calendardata']) @@ -132,27 +83,46 @@ class ReminderService { $vcalendar = $this->parseCalendarData($calendarData); if (!$vcalendar) { + $this->logger->debug('Reminder {id} does not belong to a valid calendar', [ + 'id' => $reminder['id'], + ]); + $this->backend->removeReminder($reminder['id']); + continue; + } + + try { + $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']); + } catch (MaxInstancesExceededException $e) { + $this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]); $this->backend->removeReminder($reminder['id']); continue; } - $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']); if (!$vevent) { + $this->logger->debug('Reminder {id} does not belong to a valid event', [ + 'id' => $reminder['id'], + ]); $this->backend->removeReminder($reminder['id']); continue; } if ($this->wasEventCancelled($vevent)) { + $this->logger->debug('Reminder {id} belongs to a cancelled event', [ + 'id' => $reminder['id'], + ]); $this->deleteOrProcessNext($reminder, $vevent); continue; } if (!$this->notificationProviderManager->hasProvider($reminder['type'])) { + $this->logger->debug('Reminder {id} does not belong to a valid notification provider', [ + 'id' => $reminder['id'], + ]); $this->deleteOrProcessNext($reminder, $vevent); continue; } - if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedGroupMembers', 'yes') === 'no') { + if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedUsers', 'yes') === 'no') { $users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']); } else { $users = []; @@ -163,8 +133,18 @@ class ReminderService { $users[] = $user; } + $userPrincipalEmailAddresses = []; + $userPrincipal = $this->principalConnector->getPrincipalByPath($reminder['principaluri']); + if ($userPrincipal) { + $userPrincipalEmailAddresses = $this->principalConnector->getEmailAddressesOfPrincipal($userPrincipal); + } + + $this->logger->debug('Reminder {id} will be sent to {numUsers} users', [ + 'id' => $reminder['id'], + 'numUsers' => count($users), + ]); $notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']); - $notificationProvider->send($vevent, $reminder['displayname'], $users); + $notificationProvider->send($vevent, $reminder['displayname'], $userPrincipalEmailAddresses, $users); $this->deleteOrProcessNext($reminder, $vevent); } @@ -188,18 +168,18 @@ class ReminderService { return; } - /** @var VObject\Component\VCalendar $vcalendar */ $vcalendar = $this->parseCalendarData($calendarData); if (!$vcalendar) { return; } + $calendarTimeZone = $this->getCalendarTimeZone((int)$objectData['calendarid']); $vevents = $this->getAllVEventsFromVCalendar($vcalendar); if (count($vevents) === 0) { return; } - $uid = (string) $vevents[0]->UID; + $uid = (string)$vevents[0]->UID; $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents); $masterItem = $this->getMasterItemFromListOfVEvents($vevents); $now = $this->timeFactory->getDateTime(); @@ -221,7 +201,7 @@ class ReminderService { continue; } - $alarms = $this->getRemindersForVAlarm($valarm, $objectData, + $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone, $eventHash, $alarmHash, true, true); $this->writeRemindersToDatabase($alarms); } @@ -247,6 +227,10 @@ class ReminderService { // instance. We are skipping this event from the output // entirely. return; + } catch (MaxInstancesExceededException $e) { + // The event has more than 3500 recurring-instances + // so we can ignore it + return; } while ($iterator->valid() && count($processedAlarms) < count($masterAlarms)) { @@ -265,7 +249,7 @@ class ReminderService { continue; } - if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) { + if (!\in_array((string)$valarm->ACTION, self::REMINDER_TYPES, true)) { // Action allows x-name, we don't insert reminders // into the database if they are not standard $processedAlarms[] = $alarmHash; @@ -274,6 +258,16 @@ class ReminderService { try { $triggerTime = $valarm->getEffectiveTriggerTime(); + /** + * @psalm-suppress DocblockTypeContradiction + * https://github.com/vimeo/psalm/issues/9244 + */ + if ($triggerTime->getTimezone() === false || $triggerTime->getTimezone()->getName() === 'UTC') { + $triggerTime = new DateTimeImmutable( + $triggerTime->format('Y-m-d H:i:s'), + $calendarTimeZone + ); + } } catch (InvalidDataException $e) { continue; } @@ -292,7 +286,7 @@ class ReminderService { continue; } - $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false); + $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone, $masterHash, $alarmHash, $isRecurring, false); $this->writeRemindersToDatabase($alarms); $processedAlarms[] = $alarmHash; } @@ -325,12 +319,13 @@ class ReminderService { return; } - $this->backend->cleanRemindersForEvent((int) $objectData['id']); + $this->backend->cleanRemindersForEvent((int)$objectData['id']); } /** * @param VAlarm $valarm * @param array $objectData + * @param DateTimeZone $calendarTimeZone * @param string|null $eventHash * @param string|null $alarmHash * @param bool $isRecurring @@ -338,11 +333,12 @@ class ReminderService { * @return array */ private function getRemindersForVAlarm(VAlarm $valarm, - array $objectData, - string $eventHash = null, - string $alarmHash = null, - bool $isRecurring = false, - bool $isRecurrenceException = false):array { + array $objectData, + DateTimeZone $calendarTimeZone, + ?string $eventHash = null, + ?string $alarmHash = null, + bool $isRecurring = false, + bool $isRecurrenceException = false):array { if ($eventHash === null) { $eventHash = $this->getEventHash($valarm->parent); } @@ -354,6 +350,16 @@ class ReminderService { $isRelative = $this->isAlarmRelative($valarm); /** @var DateTimeImmutable $notificationDate */ $notificationDate = $valarm->getEffectiveTriggerTime(); + /** + * @psalm-suppress DocblockTypeContradiction + * https://github.com/vimeo/psalm/issues/9244 + */ + if ($notificationDate->getTimezone() === false || $notificationDate->getTimezone()->getName() === 'UTC') { + $notificationDate = new DateTimeImmutable( + $notificationDate->format('Y-m-d H:i:s'), + $calendarTimeZone + ); + } $clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone()); $clonedNotificationDate->setTimestamp($notificationDate->getTimestamp()); @@ -362,19 +368,19 @@ class ReminderService { $alarms[] = [ 'calendar_id' => $objectData['calendarid'], 'object_id' => $objectData['id'], - 'uid' => (string) $valarm->parent->UID, + 'uid' => (string)$valarm->parent->UID, 'is_recurring' => $isRecurring, 'recurrence_id' => $recurrenceId, 'is_recurrence_exception' => $isRecurrenceException, 'event_hash' => $eventHash, 'alarm_hash' => $alarmHash, - 'type' => (string) $valarm->ACTION, + 'type' => (string)$valarm->ACTION, 'is_relative' => $isRelative, 'notification_date' => $notificationDate->getTimestamp(), 'is_repeat_based' => false, ]; - $repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0; + $repeat = isset($valarm->REPEAT) ? (int)$valarm->REPEAT->getValue() : 0; for ($i = 0; $i < $repeat; $i++) { if ($valarm->DURATION === null) { continue; @@ -384,13 +390,13 @@ class ReminderService { $alarms[] = [ 'calendar_id' => $objectData['calendarid'], 'object_id' => $objectData['id'], - 'uid' => (string) $valarm->parent->UID, + 'uid' => (string)$valarm->parent->UID, 'is_recurring' => $isRecurring, 'recurrence_id' => $recurrenceId, 'is_recurrence_exception' => $isRecurrenceException, 'event_hash' => $eventHash, 'alarm_hash' => $alarmHash, - 'type' => (string) $valarm->ACTION, + 'type' => (string)$valarm->ACTION, 'is_relative' => $isRelative, 'notification_date' => $clonedNotificationDate->getTimestamp(), 'is_repeat_based' => true, @@ -404,19 +410,26 @@ class ReminderService { * @param array $reminders */ private function writeRemindersToDatabase(array $reminders): void { + $uniqueReminders = []; foreach ($reminders as $reminder) { + $key = $reminder['notification_date'] . $reminder['event_hash'] . $reminder['type']; + if (!isset($uniqueReminders[$key])) { + $uniqueReminders[$key] = $reminder; + } + } + foreach (array_values($uniqueReminders) as $reminder) { $this->backend->insertReminder( - (int) $reminder['calendar_id'], - (int) $reminder['object_id'], + (int)$reminder['calendar_id'], + (int)$reminder['object_id'], $reminder['uid'], $reminder['is_recurring'], - (int) $reminder['recurrence_id'], + (int)$reminder['recurrence_id'], $reminder['is_recurrence_exception'], $reminder['event_hash'], $reminder['alarm_hash'], $reminder['type'], $reminder['is_relative'], - (int) $reminder['notification_date'], + (int)$reminder['notification_date'], $reminder['is_repeat_based'] ); } @@ -427,11 +440,11 @@ class ReminderService { * @param VEvent $vevent */ private function deleteOrProcessNext(array $reminder, - VObject\Component\VEvent $vevent):void { - if ($reminder['is_repeat_based'] || - !$reminder['is_recurring'] || - !$reminder['is_relative'] || - $reminder['is_recurrence_exception']) { + VObject\Component\VEvent $vevent):void { + if ($reminder['is_repeat_based'] + || !$reminder['is_recurring'] + || !$reminder['is_relative'] + || $reminder['is_recurrence_exception']) { $this->backend->removeReminder($reminder['id']); return; } @@ -439,6 +452,7 @@ class ReminderService { $vevents = $this->getAllVEventsFromVCalendar($vevent->parent); $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents); $now = $this->timeFactory->getDateTime(); + $calendarTimeZone = $this->getCalendarTimeZone((int)$reminder['calendar_id']); try { $iterator = new EventIterator($vevents, $reminder['uid']); @@ -449,49 +463,54 @@ class ReminderService { return; } - while ($iterator->valid()) { - $event = $iterator->getEventObject(); - - // Recurrence-exceptions are handled separately, so just ignore them here - if (\in_array($event, $recurrenceExceptions, true)) { - $iterator->next(); - continue; - } - - $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event); - if ($reminder['recurrence_id'] >= $recurrenceId) { - $iterator->next(); - continue; - } + try { + while ($iterator->valid()) { + $event = $iterator->getEventObject(); - foreach ($event->VALARM as $valarm) { - /** @var VAlarm $valarm */ - $alarmHash = $this->getAlarmHash($valarm); - if ($alarmHash !== $reminder['alarm_hash']) { + // Recurrence-exceptions are handled separately, so just ignore them here + if (\in_array($event, $recurrenceExceptions, true)) { + $iterator->next(); continue; } - $triggerTime = $valarm->getEffectiveTriggerTime(); - - // If effective trigger time is in the past - // just skip and generate for next event - $diff = $now->diff($triggerTime); - if ($diff->invert === 1) { + $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event); + if ($reminder['recurrence_id'] >= $recurrenceId) { + $iterator->next(); continue; } - $this->backend->removeReminder($reminder['id']); - $alarms = $this->getRemindersForVAlarm($valarm, [ - 'calendarid' => $reminder['calendar_id'], - 'id' => $reminder['object_id'], - ], $reminder['event_hash'], $alarmHash, true, false); - $this->writeRemindersToDatabase($alarms); + foreach ($event->VALARM as $valarm) { + /** @var VAlarm $valarm */ + $alarmHash = $this->getAlarmHash($valarm); + if ($alarmHash !== $reminder['alarm_hash']) { + continue; + } - // Abort generating reminders after creating one successfully - return; - } + $triggerTime = $valarm->getEffectiveTriggerTime(); + + // If effective trigger time is in the past + // just skip and generate for next event + $diff = $now->diff($triggerTime); + if ($diff->invert === 1) { + continue; + } + + $this->backend->removeReminder($reminder['id']); + $alarms = $this->getRemindersForVAlarm($valarm, [ + 'calendarid' => $reminder['calendar_id'], + 'id' => $reminder['object_id'], + ], $calendarTimeZone, $reminder['event_hash'], $alarmHash, true, false); + $this->writeRemindersToDatabase($alarms); + + // Abort generating reminders after creating one successfully + return; + } - $iterator->next(); + $iterator->next(); + } + } catch (MaxInstancesExceededException $e) { + // Using debug logger as this isn't really an error + $this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]); } $this->backend->removeReminder($reminder['id']); @@ -549,26 +568,26 @@ class ReminderService { */ private function getEventHash(VEvent $vevent):string { $properties = [ - (string) $vevent->DTSTART->serialize(), + (string)$vevent->DTSTART->serialize(), ]; if ($vevent->DTEND) { - $properties[] = (string) $vevent->DTEND->serialize(); + $properties[] = (string)$vevent->DTEND->serialize(); } if ($vevent->DURATION) { - $properties[] = (string) $vevent->DURATION->serialize(); + $properties[] = (string)$vevent->DURATION->serialize(); } if ($vevent->{'RECURRENCE-ID'}) { - $properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize(); + $properties[] = (string)$vevent->{'RECURRENCE-ID'}->serialize(); } if ($vevent->RRULE) { - $properties[] = (string) $vevent->RRULE->serialize(); + $properties[] = (string)$vevent->RRULE->serialize(); } if ($vevent->EXDATE) { - $properties[] = (string) $vevent->EXDATE->serialize(); + $properties[] = (string)$vevent->EXDATE->serialize(); } if ($vevent->RDATE) { - $properties[] = (string) $vevent->RDATE->serialize(); + $properties[] = (string)$vevent->RDATE->serialize(); } return md5(implode('::', $properties)); @@ -583,15 +602,15 @@ class ReminderService { */ private function getAlarmHash(VAlarm $valarm):string { $properties = [ - (string) $valarm->ACTION->serialize(), - (string) $valarm->TRIGGER->serialize(), + (string)$valarm->ACTION->serialize(), + (string)$valarm->TRIGGER->serialize(), ]; if ($valarm->DURATION) { - $properties[] = (string) $valarm->DURATION->serialize(); + $properties[] = (string)$valarm->DURATION->serialize(); } if ($valarm->REPEAT) { - $properties[] = (string) $valarm->REPEAT->serialize(); + $properties[] = (string)$valarm->REPEAT->serialize(); } return md5(implode('::', $properties)); @@ -604,14 +623,14 @@ class ReminderService { * @return VEvent|null */ private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar, - int $recurrenceId, - bool $isRecurrenceException):?VEvent { + int $recurrenceId, + bool $isRecurrenceException):?VEvent { $vevents = $this->getAllVEventsFromVCalendar($vcalendar); if (count($vevents) === 0) { return null; } - $uid = (string) $vevents[0]->UID; + $uid = (string)$vevents[0]->UID; $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents); $masterItem = $this->getMasterItemFromListOfVEvents($vevents); @@ -662,7 +681,7 @@ class ReminderService { */ private function getStatusOfEvent(VEvent $vevent):string { if ($vevent->STATUS) { - return (string) $vevent->STATUS; + return (string)$vevent->STATUS; } // Doesn't say so in the standard, @@ -724,6 +743,10 @@ class ReminderService { if ($child->name !== 'VEVENT') { continue; } + // Ignore invalid events with no DTSTART + if ($child->DTSTART === null) { + continue; + } $vevents[] = $child; } @@ -788,4 +811,26 @@ class ReminderService { private function isRecurring(VEvent $vevent):bool { return isset($vevent->RRULE) || isset($vevent->RDATE); } + + /** + * @param int $calendarid + * + * @return DateTimeZone + */ + private function getCalendarTimeZone(int $calendarid): DateTimeZone { + $calendarInfo = $this->caldavBackend->getCalendarById($calendarid); + $tzProp = '{urn:ietf:params:xml:ns:caldav}calendar-timezone'; + if (empty($calendarInfo[$tzProp])) { + // Defaulting to UTC + return new DateTimeZone('UTC'); + } + // This property contains a VCALENDAR with a single VTIMEZONE + /** @var string $timezoneProp */ + $timezoneProp = $calendarInfo[$tzProp]; + /** @var VObject\Component\VCalendar $vtimezoneObj */ + $vtimezoneObj = VObject\Reader::read($timezoneProp); + /** @var VObject\Component\VTimeZone $vtimezone */ + $vtimezone = $vtimezoneObj->VTIMEZONE; + return $vtimezone->getTimeZone(); + } } |