Include more information in iMIP email and show diff information on updating an eventtags/v26.0.0beta2
'OCA\\DAV\\CalDAV\\CalendarObject' => $baseDir . '/../lib/CalDAV/CalendarObject.php', | 'OCA\\DAV\\CalDAV\\CalendarObject' => $baseDir . '/../lib/CalDAV/CalendarObject.php', | ||||
'OCA\\DAV\\CalDAV\\CalendarProvider' => $baseDir . '/../lib/CalDAV/CalendarProvider.php', | 'OCA\\DAV\\CalDAV\\CalendarProvider' => $baseDir . '/../lib/CalDAV/CalendarProvider.php', | ||||
'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php', | 'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php', | ||||
'OCA\\DAV\\CalDAV\\EventComparisonService' => $baseDir . '/../lib/CalDAV/EventComparisonService.php', | |||||
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', | 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', | ||||
'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php', | 'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php', | ||||
'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => $baseDir . '/../lib/CalDAV/Integration/ExternalCalendar.php', | 'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => $baseDir . '/../lib/CalDAV/Integration/ExternalCalendar.php', | ||||
'OCA\\DAV\\CalDAV\\ResourceBooking\\RoomPrincipalBackend' => $baseDir . '/../lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php', | 'OCA\\DAV\\CalDAV\\ResourceBooking\\RoomPrincipalBackend' => $baseDir . '/../lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php', | ||||
'OCA\\DAV\\CalDAV\\RetentionService' => $baseDir . '/../lib/CalDAV/RetentionService.php', | 'OCA\\DAV\\CalDAV\\RetentionService' => $baseDir . '/../lib/CalDAV/RetentionService.php', | ||||
'OCA\\DAV\\CalDAV\\Schedule\\IMipPlugin' => $baseDir . '/../lib/CalDAV/Schedule/IMipPlugin.php', | 'OCA\\DAV\\CalDAV\\Schedule\\IMipPlugin' => $baseDir . '/../lib/CalDAV/Schedule/IMipPlugin.php', | ||||
'OCA\\DAV\\CalDAV\\Schedule\\IMipService' => $baseDir . '/../lib/CalDAV/Schedule/IMipService.php', | |||||
'OCA\\DAV\\CalDAV\\Schedule\\Plugin' => $baseDir . '/../lib/CalDAV/Schedule/Plugin.php', | 'OCA\\DAV\\CalDAV\\Schedule\\Plugin' => $baseDir . '/../lib/CalDAV/Schedule/Plugin.php', | ||||
'OCA\\DAV\\CalDAV\\Search\\SearchPlugin' => $baseDir . '/../lib/CalDAV/Search/SearchPlugin.php', | 'OCA\\DAV\\CalDAV\\Search\\SearchPlugin' => $baseDir . '/../lib/CalDAV/Search/SearchPlugin.php', | ||||
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\CompFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/CompFilter.php', | 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\CompFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/CompFilter.php', |
'OCA\\DAV\\CalDAV\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarObject.php', | 'OCA\\DAV\\CalDAV\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarObject.php', | ||||
'OCA\\DAV\\CalDAV\\CalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarProvider.php', | 'OCA\\DAV\\CalDAV\\CalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarProvider.php', | ||||
'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php', | 'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php', | ||||
'OCA\\DAV\\CalDAV\\EventComparisonService' => __DIR__ . '/..' . '/../lib/CalDAV/EventComparisonService.php', | |||||
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', | 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', | ||||
'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php', | 'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php', | ||||
'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ExternalCalendar.php', | 'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ExternalCalendar.php', | ||||
'OCA\\DAV\\CalDAV\\ResourceBooking\\RoomPrincipalBackend' => __DIR__ . '/..' . '/../lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php', | 'OCA\\DAV\\CalDAV\\ResourceBooking\\RoomPrincipalBackend' => __DIR__ . '/..' . '/../lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php', | ||||
'OCA\\DAV\\CalDAV\\RetentionService' => __DIR__ . '/..' . '/../lib/CalDAV/RetentionService.php', | 'OCA\\DAV\\CalDAV\\RetentionService' => __DIR__ . '/..' . '/../lib/CalDAV/RetentionService.php', | ||||
'OCA\\DAV\\CalDAV\\Schedule\\IMipPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Schedule/IMipPlugin.php', | 'OCA\\DAV\\CalDAV\\Schedule\\IMipPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Schedule/IMipPlugin.php', | ||||
'OCA\\DAV\\CalDAV\\Schedule\\IMipService' => __DIR__ . '/..' . '/../lib/CalDAV/Schedule/IMipService.php', | |||||
'OCA\\DAV\\CalDAV\\Schedule\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Schedule/Plugin.php', | 'OCA\\DAV\\CalDAV\\Schedule\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Schedule/Plugin.php', | ||||
'OCA\\DAV\\CalDAV\\Search\\SearchPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Search/SearchPlugin.php', | 'OCA\\DAV\\CalDAV\\Search\\SearchPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Search/SearchPlugin.php', | ||||
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\CompFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/CompFilter.php', | 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\CompFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/CompFilter.php', |
<?php | |||||
declare(strict_types=1); | |||||
/** | |||||
* @copyright 2022 Anna Larch <anna.larch@gmx.net> | |||||
* | |||||
* @author 2022 Anna Larch <anna.larch@gmx.net> | |||||
* | |||||
* @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/>. | |||||
* | |||||
*/ | |||||
namespace OCA\DAV\CalDAV; | |||||
use OCA\DAV\AppInfo\Application; | |||||
use OCA\DAV\CalDAV\Schedule\IMipService; | |||||
use OCP\AppFramework\Utility\ITimeFactory; | |||||
use OCP\IConfig; | |||||
use Sabre\VObject\Component\VCalendar; | |||||
use Sabre\VObject\Component\VEvent; | |||||
use Sabre\VObject\Component\VTimeZone; | |||||
use Sabre\VObject\Component\VTodo; | |||||
use function max; | |||||
class EventComparisonService { | |||||
/** @var string[] */ | |||||
private const EVENT_DIFF = [ | |||||
'RECURRENCE-ID', | |||||
'RRULE', | |||||
'SEQUENCE', | |||||
'LAST-MODIFIED' | |||||
]; | |||||
/** | |||||
* If found, remove the event from $eventsToFilter that | |||||
* is identical to the passed $filterEvent | |||||
* and return whether an identical event was found | |||||
* | |||||
* This function takes into account the SEQUENCE, | |||||
* RRULE, RECURRENCE-ID and LAST-MODIFIED parameters | |||||
* | |||||
* @param VEvent $filterEvent | |||||
* @param array $eventsToFilter | |||||
* @return bool true if there was an identical event found and removed, false if there wasn't | |||||
*/ | |||||
private function removeIfUnchanged(VEvent $filterEvent, array &$eventsToFilter): bool { | |||||
$filterEventData = []; | |||||
foreach(self::EVENT_DIFF as $eventDiff) { | |||||
$filterEventData[] = IMipService::readPropertyWithDefault($filterEvent, $eventDiff, ''); | |||||
} | |||||
/** @var VEvent $component */ | |||||
foreach ($eventsToFilter as $k => $eventToFilter) { | |||||
$eventToFilterData = []; | |||||
foreach(self::EVENT_DIFF as $eventDiff) { | |||||
$eventToFilterData[] = IMipService::readPropertyWithDefault($eventToFilter, $eventDiff, ''); | |||||
} | |||||
// events are identical and can be removed | |||||
if (empty(array_diff($filterEventData, $eventToFilterData))) { | |||||
unset($eventsToFilter[$k]); | |||||
return true; | |||||
} | |||||
} | |||||
return false; | |||||
} | |||||
/** | |||||
* Compare two VCalendars with each other and find all changed elements | |||||
* | |||||
* Returns an array of old and new events | |||||
* | |||||
* Old events are only detected if they are also changed | |||||
* If there is no corresponding old event for a VEvent, it | |||||
* has been newly created | |||||
* | |||||
* @param VCalendar $new | |||||
* @param VCalendar|null $old | |||||
* @return array<string, VEvent[]> | |||||
*/ | |||||
public function findModified(VCalendar $new, ?VCalendar $old): array { | |||||
$newEventComponents = $new->getComponents(); | |||||
foreach ($newEventComponents as $k => $event) { | |||||
if(!$event instanceof VEvent) { | |||||
unset($newEventComponents[$k]); | |||||
} | |||||
} | |||||
if(empty($old)) { | |||||
return ['old' => null, 'new' => $newEventComponents]; | |||||
} | |||||
$oldEventComponents = $old->getComponents(); | |||||
if(is_array($oldEventComponents) && !empty($oldEventComponents)) { | |||||
foreach ($oldEventComponents as $k => $event) { | |||||
if(!$event instanceof VEvent) { | |||||
unset($oldEventComponents[$k]); | |||||
continue; | |||||
} | |||||
if($this->removeIfUnchanged($event, $newEventComponents)) { | |||||
unset($oldEventComponents[$k]); | |||||
} | |||||
} | |||||
} | |||||
return ['old' => array_values($oldEventComponents), 'new' => array_values($newEventComponents)]; | |||||
} | |||||
} |
* @copyright Copyright (c) 2017, Georg Ehrke | * @copyright Copyright (c) 2017, Georg Ehrke | ||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). | * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). | ||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). | * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). | ||||
* @copyright 2022 Anna Larch <anna.larch@gmx.net> | |||||
* | * | ||||
* @author brad2014 <brad2014@users.noreply.github.com> | * @author brad2014 <brad2014@users.noreply.github.com> | ||||
* @author Brad Rubenstein <brad@wbr.tech> | * @author Brad Rubenstein <brad@wbr.tech> | ||||
* @author Roeland Jago Douma <roeland@famdouma.nl> | * @author Roeland Jago Douma <roeland@famdouma.nl> | ||||
* @author Thomas Citharel <nextcloud@tcit.fr> | * @author Thomas Citharel <nextcloud@tcit.fr> | ||||
* @author Thomas Müller <thomas.mueller@tmit.eu> | * @author Thomas Müller <thomas.mueller@tmit.eu> | ||||
* @author Anna Larch <anna.larch@gmx.net> | |||||
* | * | ||||
* @license AGPL-3.0 | * @license AGPL-3.0 | ||||
* | * | ||||
*/ | */ | ||||
namespace OCA\DAV\CalDAV\Schedule; | namespace OCA\DAV\CalDAV\Schedule; | ||||
use OCA\DAV\CalDAV\CalendarObject; | |||||
use OCA\DAV\CalDAV\EventComparisonService; | |||||
use OCP\AppFramework\Utility\ITimeFactory; | use OCP\AppFramework\Utility\ITimeFactory; | ||||
use OCP\Defaults; | use OCP\Defaults; | ||||
use OCP\IConfig; | use OCP\IConfig; | ||||
use OCP\Util; | use OCP\Util; | ||||
use Psr\Log\LoggerInterface; | use Psr\Log\LoggerInterface; | ||||
use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin; | use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin; | ||||
use Sabre\DAV; | |||||
use Sabre\DAV\INode; | |||||
use Sabre\VObject\Component\VCalendar; | use Sabre\VObject\Component\VCalendar; | ||||
use Sabre\VObject\Component\VEvent; | use Sabre\VObject\Component\VEvent; | ||||
use Sabre\VObject\Component\VTimeZone; | |||||
use Sabre\VObject\DateTimeParser; | use Sabre\VObject\DateTimeParser; | ||||
use Sabre\VObject\ITip\Message; | use Sabre\VObject\ITip\Message; | ||||
use Sabre\VObject\Parameter; | use Sabre\VObject\Parameter; | ||||
use Sabre\VObject\Property; | use Sabre\VObject\Property; | ||||
use Sabre\VObject\Reader; | |||||
use Sabre\VObject\Recur\EventIterator; | use Sabre\VObject\Recur\EventIterator; | ||||
/** | /** | ||||
* @license http://sabre.io/license/ Modified BSD License | * @license http://sabre.io/license/ Modified BSD License | ||||
*/ | */ | ||||
class IMipPlugin extends SabreIMipPlugin { | class IMipPlugin extends SabreIMipPlugin { | ||||
/** @var string */ | |||||
private $userId; | |||||
/** @var IConfig */ | |||||
private $config; | |||||
/** @var IMailer */ | |||||
private $mailer; | |||||
private ?string $userId; | |||||
private IConfig $config; | |||||
private IMailer $mailer; | |||||
private LoggerInterface $logger; | private LoggerInterface $logger; | ||||
/** @var ITimeFactory */ | |||||
private $timeFactory; | |||||
/** @var L10NFactory */ | |||||
private $l10nFactory; | |||||
/** @var IURLGenerator */ | |||||
private $urlGenerator; | |||||
/** @var ISecureRandom */ | |||||
private $random; | |||||
/** @var IDBConnection */ | |||||
private $db; | |||||
/** @var Defaults */ | |||||
private $defaults; | |||||
/** @var IUserManager */ | |||||
private $userManager; | |||||
private ITimeFactory $timeFactory; | |||||
private Defaults $defaults; | |||||
private IUserManager $userManager; | |||||
private ?VCalendar $vCalendar = null; | |||||
private IMipService $imipService; | |||||
public const MAX_DATE = '2038-01-01'; | public const MAX_DATE = '2038-01-01'; | ||||
public const METHOD_REQUEST = 'request'; | public const METHOD_REQUEST = 'request'; | ||||
public const METHOD_REPLY = 'reply'; | public const METHOD_REPLY = 'reply'; | ||||
public const METHOD_CANCEL = 'cancel'; | public const METHOD_CANCEL = 'cancel'; | ||||
public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages | public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages | ||||
private EventComparisonService $eventComparisonService; | |||||
public function __construct(IConfig $config, IMailer $mailer, | |||||
public function __construct(IConfig $config, | |||||
IMailer $mailer, | |||||
LoggerInterface $logger, | LoggerInterface $logger, | ||||
ITimeFactory $timeFactory, L10NFactory $l10nFactory, | |||||
IURLGenerator $urlGenerator, Defaults $defaults, | |||||
ISecureRandom $random, IDBConnection $db, IUserManager $userManager, | |||||
$userId) { | |||||
ITimeFactory $timeFactory, | |||||
Defaults $defaults, | |||||
IUserManager $userManager, | |||||
$userId, | |||||
IMipService $imipService, | |||||
EventComparisonService $eventComparisonService) { | |||||
parent::__construct(''); | parent::__construct(''); | ||||
$this->userId = $userId; | $this->userId = $userId; | ||||
$this->config = $config; | $this->config = $config; | ||||
$this->mailer = $mailer; | $this->mailer = $mailer; | ||||
$this->logger = $logger; | $this->logger = $logger; | ||||
$this->timeFactory = $timeFactory; | $this->timeFactory = $timeFactory; | ||||
$this->l10nFactory = $l10nFactory; | |||||
$this->urlGenerator = $urlGenerator; | |||||
$this->random = $random; | |||||
$this->db = $db; | |||||
$this->defaults = $defaults; | $this->defaults = $defaults; | ||||
$this->userManager = $userManager; | $this->userManager = $userManager; | ||||
$this->imipService = $imipService; | |||||
$this->eventComparisonService = $eventComparisonService; | |||||
} | |||||
public function initialize(DAV\Server $server): void { | |||||
parent::initialize($server); | |||||
$server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10); | |||||
} | |||||
/** | |||||
* Check quota before writing content | |||||
* | |||||
* @param string $uri target file URI | |||||
* @param INode $node Sabre Node | |||||
* @param resource $data data | |||||
* @param bool $modified modified | |||||
*/ | |||||
public function beforeWriteContent($uri, INode $node, $data, $modified): void { | |||||
if(!$node instanceof CalendarObject) { | |||||
return; | |||||
} | |||||
/** @var VCalendar $vCalendar */ | |||||
$vCalendar = Reader::read($node->get()); | |||||
$this->setVCalendar($vCalendar); | |||||
} | } | ||||
/** | /** | ||||
return; | return; | ||||
} | } | ||||
$summary = $iTipMessage->message->VEVENT->SUMMARY; | |||||
if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') { | |||||
return; | |||||
} | |||||
if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') { | |||||
if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto' | |||||
|| parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') { | |||||
return; | return; | ||||
} | } | ||||
// don't send out mails for events that already took place | // don't send out mails for events that already took place | ||||
$lastOccurrence = $this->getLastOccurrence($iTipMessage->message); | |||||
$lastOccurrence = $this->imipService->getLastOccurrence($iTipMessage->message); | |||||
$currentTime = $this->timeFactory->getTime(); | $currentTime = $this->timeFactory->getTime(); | ||||
if ($lastOccurrence < $currentTime) { | if ($lastOccurrence < $currentTime) { | ||||
return; | return; | ||||
} | } | ||||
// Strip off mailto: | // Strip off mailto: | ||||
$sender = substr($iTipMessage->sender, 7); | |||||
$recipient = substr($iTipMessage->recipient, 7); | $recipient = substr($iTipMessage->recipient, 7); | ||||
if (!$this->mailer->validateMailAddress($recipient)) { | if (!$this->mailer->validateMailAddress($recipient)) { | ||||
// Nothing to send if the recipient doesn't have a valid email address | // Nothing to send if the recipient doesn't have a valid email address | ||||
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; | $iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; | ||||
return; | return; | ||||
} | } | ||||
$recipientName = $iTipMessage->recipientName ?: null; | $recipientName = $iTipMessage->recipientName ?: null; | ||||
$newEvents = $iTipMessage->message; | |||||
$oldEvents = $this->getVCalendar(); | |||||
$modified = $this->eventComparisonService->findModified($newEvents, $oldEvents); | |||||
/** @var VEvent $vEvent */ | |||||
$vEvent = array_pop($modified['new']); | |||||
/** @var VEvent $oldVevent */ | |||||
$oldVevent = !empty($modified['old']) && is_array($modified['old']) ? array_pop($modified['old']) : null; | |||||
// No changed events after all - this shouldn't happen if there is significant change yet here we are | |||||
// The scheduling status is debatable | |||||
if(empty($vEvent)) { | |||||
$this->logger->warning('iTip message said the change was significant but comparison did not detect any updated VEvents'); | |||||
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; | |||||
return; | |||||
} | |||||
// we (should) have one event component left | |||||
// as the ITip\Broker creates one iTip message per change | |||||
// and triggers the "schedule" event once per message | |||||
// we also might not have an old event as this could be a new | |||||
// invitation, or a new recurrence exception | |||||
$attendee = $this->imipService->getCurrentAttendee($iTipMessage); | |||||
$this->imipService->setL10n($attendee); | |||||
// Build the sender name. | |||||
// Due to a bug in sabre, the senderName property for an iTIP message | |||||
// can actually also be a VObject Property | |||||
/** @var Parameter|string|null $senderName */ | /** @var Parameter|string|null $senderName */ | ||||
$senderName = $iTipMessage->senderName ?: null; | $senderName = $iTipMessage->senderName ?: null; | ||||
if($senderName instanceof Parameter) { | if($senderName instanceof Parameter) { | ||||
if ($senderName === null || empty(trim($senderName))) { | if ($senderName === null || empty(trim($senderName))) { | ||||
$senderName = $this->userManager->getDisplayName($this->userId); | $senderName = $this->userManager->getDisplayName($this->userId); | ||||
} | } | ||||
$sender = substr($iTipMessage->sender, 7); | |||||
/** @var VEvent $vevent */ | |||||
$vevent = $iTipMessage->message->VEVENT; | |||||
$attendee = $this->getCurrentAttendee($iTipMessage); | |||||
$defaultLang = $this->l10nFactory->findGenericLanguage(); | |||||
$lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee); | |||||
$l10n = $this->l10nFactory->get('dav', $lang); | |||||
$meetingAttendeeName = $recipientName ?: $recipient; | |||||
$meetingInviteeName = $senderName ?: $sender; | |||||
$meetingTitle = $vevent->SUMMARY; | |||||
$meetingDescription = $vevent->DESCRIPTION; | |||||
$meetingUrl = $vevent->URL; | |||||
$meetingLocation = $vevent->LOCATION; | |||||
$defaultVal = '--'; | |||||
$method = self::METHOD_REQUEST; | |||||
switch (strtolower($iTipMessage->method)) { | switch (strtolower($iTipMessage->method)) { | ||||
case self::METHOD_REPLY: | case self::METHOD_REPLY: | ||||
$method = self::METHOD_REPLY; | $method = self::METHOD_REPLY; | ||||
$data = $this->imipService->buildBodyData($vEvent, $oldVevent); | |||||
break; | break; | ||||
case self::METHOD_CANCEL: | case self::METHOD_CANCEL: | ||||
$method = self::METHOD_CANCEL; | $method = self::METHOD_CANCEL; | ||||
$data = $this->imipService->buildCancelledBodyData($vEvent); | |||||
break; | |||||
default: | |||||
$method = self::METHOD_REQUEST; | |||||
$data = $this->imipService->buildBodyData($vEvent, $oldVevent); | |||||
break; | break; | ||||
} | } | ||||
$data = [ | |||||
'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal, | |||||
'invitee_name' => (string)$meetingInviteeName ?: $defaultVal, | |||||
'meeting_title' => (string)$meetingTitle ?: $defaultVal, | |||||
'meeting_description' => (string)$meetingDescription ?: $defaultVal, | |||||
'meeting_url' => (string)$meetingUrl ?: $defaultVal, | |||||
]; | |||||
$data['attendee_name'] = ($recipientName ?: $recipient); | |||||
$data['invitee_name'] = ($senderName ?: $sender); | |||||
$fromEMail = Util::getDefaultEmailAddress('invitations-noreply'); | $fromEMail = Util::getDefaultEmailAddress('invitations-noreply'); | ||||
$fromName = $l10n->t('%1$s via %2$s', [$senderName ?? $this->userId, $this->defaults->getName()]); | |||||
$fromName = $this->imipService->getFrom($senderName, $this->defaults->getName()); | |||||
$message = $this->mailer->createMessage() | $message = $this->mailer->createMessage() | ||||
->setFrom([$fromEMail => $fromName]) | ->setFrom([$fromEMail => $fromName]) | ||||
$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data); | $template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data); | ||||
$template->addHeader(); | $template->addHeader(); | ||||
$summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event'); | |||||
$this->addSubjectAndHeading($template, $l10n, $method, $summary); | |||||
$this->addBulletList($template, $l10n, $vevent); | |||||
$this->imipService->addSubjectAndHeading($template, $method, $data['invitee_name'], $data['meeting_title']); | |||||
$this->imipService->addBulletList($template, $vEvent, $data); | |||||
// Only add response buttons to invitation requests: Fix Issue #11230 | // Only add response buttons to invitation requests: Fix Issue #11230 | ||||
if (($method == self::METHOD_REQUEST) && $this->getAttendeeRsvpOrReqForParticipant($attendee)) { | |||||
if (strcasecmp($method, self::METHOD_REQUEST) === 0 && $this->imipService->getAttendeeRsvpOrReqForParticipant($attendee)) { | |||||
/* | /* | ||||
** Only offer invitation accept/reject buttons, which link back to the | ** Only offer invitation accept/reject buttons, which link back to the | ||||
** nextcloud server, to recipients who can access the nextcloud server via | ** nextcloud server, to recipients who can access the nextcloud server via | ||||
** To suppress URLs entirely, set invitation_link_recipients to boolean "no". | ** To suppress URLs entirely, set invitation_link_recipients to boolean "no". | ||||
*/ | */ | ||||
$recipientDomain = substr(strrchr($recipient, "@"), 1); | |||||
$recipientDomain = substr(strrchr($recipient, '@'), 1); | |||||
$invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes')))); | $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes')))); | ||||
if (strcmp('yes', $invitationLinkRecipients[0]) === 0 | if (strcmp('yes', $invitationLinkRecipients[0]) === 0 | ||||
|| in_array(strtolower($recipient), $invitationLinkRecipients) | |||||
|| in_array(strtolower($recipientDomain), $invitationLinkRecipients)) { | |||||
$this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence); | |||||
|| in_array(strtolower($recipient), $invitationLinkRecipients) | |||||
|| in_array(strtolower($recipientDomain), $invitationLinkRecipients)) { | |||||
$token = $this->imipService->createInvitationToken($iTipMessage, $vEvent, $lastOccurrence); | |||||
$this->imipService->addResponseButtons($template, $token); | |||||
$this->imipService->addMoreOptionsButton($template, $token); | |||||
} | } | ||||
} | } | ||||
$message->useTemplate($template); | $message->useTemplate($template); | ||||
$vCalendar = $this->imipService->generateVCalendar($iTipMessage, $vEvent); | |||||
$attachment = $this->mailer->createAttachment( | $attachment = $this->mailer->createAttachment( | ||||
$iTipMessage->message->serialize(), | |||||
'event.ics',// TODO(leon): Make file name unique, e.g. add event id | |||||
$vCalendar->serialize(), | |||||
'event.ics', | |||||
'text/calendar; method=' . $iTipMessage->method | 'text/calendar; method=' . $iTipMessage->method | ||||
); | ); | ||||
$message->attach($attachment); | $message->attach($attachment); | ||||
try { | try { | ||||
$failed = $this->mailer->send($message); | $failed = $this->mailer->send($message); | ||||
$iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip'; | $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip'; | ||||
if ($failed) { | |||||
if (!empty($failed)) { | |||||
$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]); | $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]); | ||||
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; | $iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; | ||||
} | } | ||||
} | } | ||||
/** | /** | ||||
* check if event took place in the past already | |||||
* @param VCalendar $vObject | |||||
* @return int | |||||
*/ | |||||
private function getLastOccurrence(VCalendar $vObject) { | |||||
/** @var VEvent $component */ | |||||
$component = $vObject->VEVENT; | |||||
$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp(); | |||||
// Finding the last occurrence is a bit harder | |||||
if (!isset($component->RRULE)) { | |||||
if (isset($component->DTEND)) { | |||||
$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp(); | |||||
} elseif (isset($component->DURATION)) { | |||||
/** @var \DateTime $endDate */ | |||||
$endDate = clone $component->DTSTART->getDateTime(); | |||||
// $component->DTEND->getDateTime() returns DateTimeImmutable | |||||
$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue())); | |||||
$lastOccurrence = $endDate->getTimestamp(); | |||||
} elseif (!$component->DTSTART->hasTime()) { | |||||
/** @var \DateTime $endDate */ | |||||
$endDate = clone $component->DTSTART->getDateTime(); | |||||
// $component->DTSTART->getDateTime() returns DateTimeImmutable | |||||
$endDate = $endDate->modify('+1 day'); | |||||
$lastOccurrence = $endDate->getTimestamp(); | |||||
} else { | |||||
$lastOccurrence = $firstOccurrence; | |||||
} | |||||
} else { | |||||
$it = new EventIterator($vObject, (string)$component->UID); | |||||
$maxDate = new \DateTime(self::MAX_DATE); | |||||
if ($it->isInfinite()) { | |||||
$lastOccurrence = $maxDate->getTimestamp(); | |||||
} else { | |||||
$end = $it->getDtEnd(); | |||||
while ($it->valid() && $end < $maxDate) { | |||||
$end = $it->getDtEnd(); | |||||
$it->next(); | |||||
} | |||||
$lastOccurrence = $end->getTimestamp(); | |||||
} | |||||
} | |||||
return $lastOccurrence; | |||||
} | |||||
/** | |||||
* @param Message $iTipMessage | |||||
* @return null|Property | |||||
*/ | |||||
private function getCurrentAttendee(Message $iTipMessage) { | |||||
/** @var VEvent $vevent */ | |||||
$vevent = $iTipMessage->message->VEVENT; | |||||
$attendees = $vevent->select('ATTENDEE'); | |||||
foreach ($attendees as $attendee) { | |||||
/** @var Property $attendee */ | |||||
if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) { | |||||
return $attendee; | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
/** | |||||
* @param string $default | |||||
* @param Property|null $attendee | |||||
* @return string | |||||
* @return ?VCalendar | |||||
*/ | */ | ||||
private function getAttendeeLangOrDefault($default, Property $attendee = null) { | |||||
if ($attendee !== null) { | |||||
$lang = $attendee->offsetGet('LANGUAGE'); | |||||
if ($lang instanceof Parameter) { | |||||
return $lang->getValue(); | |||||
} | |||||
} | |||||
return $default; | |||||
public function getVCalendar(): ?VCalendar { | |||||
return $this->vCalendar; | |||||
} | } | ||||
/** | /** | ||||
* @param Property|null $attendee | |||||
* @return bool | |||||
* @param ?VCalendar $vCalendar | |||||
*/ | */ | ||||
private function getAttendeeRsvpOrReqForParticipant(Property $attendee = null) { | |||||
if ($attendee !== null) { | |||||
$rsvp = $attendee->offsetGet('RSVP'); | |||||
if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) { | |||||
return true; | |||||
} | |||||
$role = $attendee->offsetGet('ROLE'); | |||||
// @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16 | |||||
// Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set | |||||
if ($role === null | |||||
|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0)) | |||||
|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0)) | |||||
) { | |||||
return true; | |||||
} | |||||
} | |||||
// RFC 5545 3.2.17: default RSVP is false | |||||
return false; | |||||
public function setVCalendar(?VCalendar $vCalendar): void { | |||||
$this->vCalendar = $vCalendar; | |||||
} | } | ||||
/** | |||||
* @param IL10N $l10n | |||||
* @param VEvent $vevent | |||||
*/ | |||||
private function generateWhenString(IL10N $l10n, VEvent $vevent) { | |||||
$dtstart = $vevent->DTSTART; | |||||
if (isset($vevent->DTEND)) { | |||||
$dtend = $vevent->DTEND; | |||||
} elseif (isset($vevent->DURATION)) { | |||||
$isFloating = $vevent->DTSTART->isFloating(); | |||||
$dtend = clone $vevent->DTSTART; | |||||
$endDateTime = $dtend->getDateTime(); | |||||
$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue())); | |||||
$dtend->setDateTime($endDateTime, $isFloating); | |||||
} elseif (!$vevent->DTSTART->hasTime()) { | |||||
$isFloating = $vevent->DTSTART->isFloating(); | |||||
$dtend = clone $vevent->DTSTART; | |||||
$endDateTime = $dtend->getDateTime(); | |||||
$endDateTime = $endDateTime->modify('+1 day'); | |||||
$dtend->setDateTime($endDateTime, $isFloating); | |||||
} else { | |||||
$dtend = clone $vevent->DTSTART; | |||||
} | |||||
$isAllDay = $dtstart instanceof Property\ICalendar\Date; | |||||
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */ | |||||
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */ | |||||
/** @var \DateTimeImmutable $dtstartDt */ | |||||
$dtstartDt = $dtstart->getDateTime(); | |||||
/** @var \DateTimeImmutable $dtendDt */ | |||||
$dtendDt = $dtend->getDateTime(); | |||||
$diff = $dtstartDt->diff($dtendDt); | |||||
$dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM)); | |||||
$dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM)); | |||||
if ($isAllDay) { | |||||
// One day event | |||||
if ($diff->days === 1) { | |||||
return $l10n->l('date', $dtstartDt, ['width' => 'medium']); | |||||
} | |||||
// DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05, | |||||
// the email should show 2020-01-01 to 2020-01-04. | |||||
$dtendDt->modify('-1 day'); | |||||
//event that spans over multiple days | |||||
$localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']); | |||||
$localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']); | |||||
return $localeStart . ' - ' . $localeEnd; | |||||
} | |||||
/** @var Property\ICalendar\DateTime $dtstart */ | |||||
/** @var Property\ICalendar\DateTime $dtend */ | |||||
$isFloating = $dtstart->isFloating(); | |||||
$startTimezone = $endTimezone = null; | |||||
if (!$isFloating) { | |||||
$prop = $dtstart->offsetGet('TZID'); | |||||
if ($prop instanceof Parameter) { | |||||
$startTimezone = $prop->getValue(); | |||||
} | |||||
$prop = $dtend->offsetGet('TZID'); | |||||
if ($prop instanceof Parameter) { | |||||
$endTimezone = $prop->getValue(); | |||||
} | |||||
} | |||||
$localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' . | |||||
$l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']); | |||||
// always show full date with timezone if timezones are different | |||||
if ($startTimezone !== $endTimezone) { | |||||
$localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']); | |||||
return $localeStart . ' (' . $startTimezone . ') - ' . | |||||
$localeEnd . ' (' . $endTimezone . ')'; | |||||
} | |||||
// show only end time if date is the same | |||||
if ($this->isDayEqual($dtstartDt, $dtendDt)) { | |||||
$localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']); | |||||
} else { | |||||
$localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' . | |||||
$l10n->l('datetime', $dtendDt, ['width' => 'medium|short']); | |||||
} | |||||
return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')'; | |||||
} | |||||
/** | |||||
* @param \DateTime $dtStart | |||||
* @param \DateTime $dtEnd | |||||
* @return bool | |||||
*/ | |||||
private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) { | |||||
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d'); | |||||
} | |||||
/** | |||||
* @param IEMailTemplate $template | |||||
* @param IL10N $l10n | |||||
* @param string $method | |||||
* @param string $summary | |||||
*/ | |||||
private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, | |||||
$method, $summary) { | |||||
if ($method === self::METHOD_CANCEL) { | |||||
// TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}" | |||||
$template->setSubject($l10n->t('Cancelled: %1$s', [$summary])); | |||||
$template->addHeading($l10n->t('Invitation canceled')); | |||||
} elseif ($method === self::METHOD_REPLY) { | |||||
// TRANSLATORS Subject for email, when an invitation is updated. Ex: "Re: {{Event Name}}" | |||||
$template->setSubject($l10n->t('Re: %1$s', [$summary])); | |||||
$template->addHeading($l10n->t('Invitation updated')); | |||||
} else { | |||||
// TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}" | |||||
$template->setSubject($l10n->t('Invitation: %1$s', [$summary])); | |||||
$template->addHeading($l10n->t('Invitation')); | |||||
} | |||||
} | |||||
/** | |||||
* @param IEMailTemplate $template | |||||
* @param IL10N $l10n | |||||
* @param VEVENT $vevent | |||||
*/ | |||||
private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) { | |||||
if ($vevent->SUMMARY) { | |||||
$template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'), | |||||
$this->getAbsoluteImagePath('caldav/title.png'), '', '', self::IMIP_INDENT); | |||||
} | |||||
$meetingWhen = $this->generateWhenString($l10n, $vevent); | |||||
if ($meetingWhen) { | |||||
$template->addBodyListItem($meetingWhen, $l10n->t('Time:'), | |||||
$this->getAbsoluteImagePath('caldav/time.png'), '', '', self::IMIP_INDENT); | |||||
} | |||||
if ($vevent->LOCATION) { | |||||
$template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'), | |||||
$this->getAbsoluteImagePath('caldav/location.png'), '', '', self::IMIP_INDENT); | |||||
} | |||||
if ($vevent->URL) { | |||||
$url = $vevent->URL->getValue(); | |||||
$template->addBodyListItem(sprintf('<a href="%s">%s</a>', | |||||
htmlspecialchars($url), | |||||
htmlspecialchars($url)), | |||||
$l10n->t('Link:'), | |||||
$this->getAbsoluteImagePath('caldav/link.png'), | |||||
$url, '', self::IMIP_INDENT); | |||||
} | |||||
$this->addAttendees($template, $l10n, $vevent); | |||||
/* Put description last, like an email body, since it can be arbitrarily long */ | |||||
if ($vevent->DESCRIPTION) { | |||||
$template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'), | |||||
$this->getAbsoluteImagePath('caldav/description.png'), '', '', self::IMIP_INDENT); | |||||
} | |||||
} | |||||
/** | |||||
* addAttendees: add organizer and attendee names/emails to iMip mail. | |||||
* | |||||
* Enable with DAV setting: invitation_list_attendees (default: no) | |||||
* | |||||
* The default is 'no', which matches old behavior, and is privacy preserving. | |||||
* | |||||
* To enable including attendees in invitation emails: | |||||
* % php occ config:app:set dav invitation_list_attendees --value yes | |||||
* | |||||
* @param IEMailTemplate $template | |||||
* @param IL10N $l10n | |||||
* @param Message $iTipMessage | |||||
* @param int $lastOccurrence | |||||
* @author brad2014 on github.com | |||||
*/ | |||||
private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) { | |||||
if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') { | |||||
return; | |||||
} | |||||
if (isset($vevent->ORGANIZER)) { | |||||
/** @var Property\ICalendar\CalAddress $organizer */ | |||||
$organizer = $vevent->ORGANIZER; | |||||
$organizerURI = $organizer->getNormalizedValue(); | |||||
[$scheme,$organizerEmail] = explode(':', $organizerURI, 2); # strip off scheme mailto: | |||||
/** @var string|null $organizerName */ | |||||
$organizerName = isset($organizer['CN']) ? $organizer['CN'] : null; | |||||
$organizerHTML = sprintf('<a href="%s">%s</a>', | |||||
htmlspecialchars($organizerURI), | |||||
htmlspecialchars($organizerName ?: $organizerEmail)); | |||||
$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail); | |||||
if (isset($organizer['PARTSTAT'])) { | |||||
/** @var Parameter $partstat */ | |||||
$partstat = $organizer['PARTSTAT']; | |||||
if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) { | |||||
$organizerHTML .= ' ✔︎'; | |||||
$organizerText .= ' ✔︎'; | |||||
} | |||||
} | |||||
$template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'), | |||||
$this->getAbsoluteImagePath('caldav/organizer.png'), | |||||
$organizerText, '', self::IMIP_INDENT); | |||||
} | |||||
$attendees = $vevent->select('ATTENDEE'); | |||||
if (count($attendees) === 0) { | |||||
return; | |||||
} | |||||
$attendeesHTML = []; | |||||
$attendeesText = []; | |||||
foreach ($attendees as $attendee) { | |||||
$attendeeURI = $attendee->getNormalizedValue(); | |||||
[$scheme,$attendeeEmail] = explode(':', $attendeeURI, 2); # strip off scheme mailto: | |||||
$attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null; | |||||
$attendeeHTML = sprintf('<a href="%s">%s</a>', | |||||
htmlspecialchars($attendeeURI), | |||||
htmlspecialchars($attendeeName ?: $attendeeEmail)); | |||||
$attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail); | |||||
if (isset($attendee['PARTSTAT']) | |||||
&& strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) { | |||||
$attendeeHTML .= ' ✔︎'; | |||||
$attendeeText .= ' ✔︎'; | |||||
} | |||||
array_push($attendeesHTML, $attendeeHTML); | |||||
array_push($attendeesText, $attendeeText); | |||||
} | |||||
$template->addBodyListItem(implode('<br/>', $attendeesHTML), $l10n->t('Attendees:'), | |||||
$this->getAbsoluteImagePath('caldav/attendees.png'), | |||||
implode("\n", $attendeesText), '', self::IMIP_INDENT); | |||||
} | |||||
/** | |||||
* @param IEMailTemplate $template | |||||
* @param IL10N $l10n | |||||
* @param Message $iTipMessage | |||||
* @param int $lastOccurrence | |||||
*/ | |||||
private function addResponseButtons(IEMailTemplate $template, IL10N $l10n, | |||||
Message $iTipMessage, $lastOccurrence) { | |||||
$token = $this->createInvitationToken($iTipMessage, $lastOccurrence); | |||||
$template->addBodyButtonGroup( | |||||
$l10n->t('Accept'), | |||||
$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [ | |||||
'token' => $token, | |||||
]), | |||||
$l10n->t('Decline'), | |||||
$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [ | |||||
'token' => $token, | |||||
]) | |||||
); | |||||
$moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [ | |||||
'token' => $token, | |||||
]); | |||||
$html = vsprintf('<small><a href="%s">%s</a></small>', [ | |||||
$moreOptionsURL, $l10n->t('More options …') | |||||
]); | |||||
$text = $l10n->t('More options at %s', [$moreOptionsURL]); | |||||
$template->addBodyText($html, $text); | |||||
} | |||||
/** | |||||
* @param string $path | |||||
* @return string | |||||
*/ | |||||
private function getAbsoluteImagePath($path) { | |||||
return $this->urlGenerator->getAbsoluteURL( | |||||
$this->urlGenerator->imagePath('core', $path) | |||||
); | |||||
} | |||||
/** | |||||
* @param Message $iTipMessage | |||||
* @param int $lastOccurrence | |||||
* @return string | |||||
*/ | |||||
private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string { | |||||
$token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC); | |||||
/** @var VEvent $vevent */ | |||||
$vevent = $iTipMessage->message->VEVENT; | |||||
$attendee = $iTipMessage->recipient; | |||||
$organizer = $iTipMessage->sender; | |||||
$sequence = $iTipMessage->sequence; | |||||
$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? | |||||
$vevent->{'RECURRENCE-ID'}->serialize() : null; | |||||
$uid = $vevent->{'UID'}; | |||||
$query = $this->db->getQueryBuilder(); | |||||
$query->insert('calendar_invitations') | |||||
->values([ | |||||
'token' => $query->createNamedParameter($token), | |||||
'attendee' => $query->createNamedParameter($attendee), | |||||
'organizer' => $query->createNamedParameter($organizer), | |||||
'sequence' => $query->createNamedParameter($sequence), | |||||
'recurrenceid' => $query->createNamedParameter($recurrenceId), | |||||
'expiration' => $query->createNamedParameter($lastOccurrence), | |||||
'uid' => $query->createNamedParameter($uid) | |||||
]) | |||||
->execute(); | |||||
return $token; | |||||
} | |||||
} | } |
<?php | |||||
declare(strict_types=1); | |||||
/* | |||||
* DAV App | |||||
* | |||||
* @copyright 2022 Anna Larch <anna.larch@gmx.net> | |||||
* | |||||
* @author Anna Larch <anna.larch@gmx.net> | |||||
* | |||||
* This library is free software; you can redistribute it and/or | |||||
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE | |||||
* License as published by the Free Software Foundation; either | |||||
* version 3 of the License, or any later version. | |||||
* | |||||
* This library 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 library. If not, see <http://www.gnu.org/licenses/>. | |||||
*/ | |||||
namespace OCA\DAV\CalDAV\Schedule; | |||||
use OC\URLGenerator; | |||||
use OCP\IConfig; | |||||
use OCP\IDBConnection; | |||||
use OCP\IL10N; | |||||
use OCP\L10N\IFactory as L10NFactory; | |||||
use OCP\Mail\IEMailTemplate; | |||||
use OCP\Security\ISecureRandom; | |||||
use Sabre\VObject\Component\VCalendar; | |||||
use Sabre\VObject\Component\VEvent; | |||||
use Sabre\VObject\DateTimeParser; | |||||
use Sabre\VObject\ITip\Message; | |||||
use Sabre\VObject\Parameter; | |||||
use Sabre\VObject\Property; | |||||
use Sabre\VObject\Recur\EventIterator; | |||||
class IMipService { | |||||
private URLGenerator $urlGenerator; | |||||
private IConfig $config; | |||||
private IDBConnection $db; | |||||
private ISecureRandom $random; | |||||
private L10NFactory $l10nFactory; | |||||
private IL10N $l10n; | |||||
/** @var string[] */ | |||||
private const STRING_DIFF = [ | |||||
'meeting_title' => 'SUMMARY', | |||||
'meeting_description' => 'DESCRIPTION', | |||||
'meeting_url' => 'URL', | |||||
'meeting_location' => 'LOCATION' | |||||
]; | |||||
public function __construct(URLGenerator $urlGenerator, | |||||
IConfig $config, | |||||
IDBConnection $db, | |||||
ISecureRandom $random, | |||||
L10NFactory $l10nFactory) { | |||||
$this->urlGenerator = $urlGenerator; | |||||
$this->config = $config; | |||||
$this->db = $db; | |||||
$this->random = $random; | |||||
$this->l10nFactory = $l10nFactory; | |||||
$default = $this->l10nFactory->findGenericLanguage(); | |||||
$this->l10n = $this->l10nFactory->get('dav', $default); | |||||
} | |||||
/** | |||||
* @param string $senderName | |||||
* @param $default | |||||
* @return string | |||||
*/ | |||||
public function getFrom(string $senderName, $default): string { | |||||
return $this->l10n->t('%1$s via %2$s', [$senderName, $default]); | |||||
} | |||||
public static function readPropertyWithDefault(VEvent $vevent, string $property, string $default) { | |||||
if (isset($vevent->$property)) { | |||||
$value = $vevent->$property->getValue(); | |||||
if (!empty($value)) { | |||||
return $value; | |||||
} | |||||
} | |||||
return $default; | |||||
} | |||||
private function generateDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string { | |||||
$strikethrough = "<span style='text-decoration: line-through'>%s</span><br />%s"; | |||||
if (!isset($vevent->$property)) { | |||||
return $default; | |||||
} | |||||
$newstring = $vevent->$property->getValue(); | |||||
if(isset($oldVEvent->$property)) { | |||||
$oldstring = $oldVEvent->$property->getValue(); | |||||
return sprintf($strikethrough, $oldstring, $newstring); | |||||
} | |||||
return $newstring; | |||||
} | |||||
/** | |||||
* @param VEvent $vEvent | |||||
* @param VEvent|null $oldVEvent | |||||
* @return array | |||||
*/ | |||||
public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array { | |||||
$defaultVal = ''; | |||||
$data = []; | |||||
$data['meeting_when'] = $this->generateWhenString($vEvent); | |||||
foreach(self::STRING_DIFF as $key => $property) { | |||||
$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal); | |||||
} | |||||
$data['meeting_url_html'] = self::readPropertyWithDefault($vEvent, 'URL', $defaultVal); | |||||
if(!empty($oldVEvent)) { | |||||
$oldMeetingWhen = $this->generateWhenString($oldVEvent); | |||||
$data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']); | |||||
$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']); | |||||
$data['meeting_location_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']); | |||||
$oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal); | |||||
$data['meeting_url_html'] = !empty($oldUrl) ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url']; | |||||
$data['meeting_when_html'] = | |||||
($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null) | |||||
? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when']) | |||||
: $data['meeting_when']; | |||||
} | |||||
return $data; | |||||
} | |||||
/** | |||||
* @param IL10N $this->l10n | |||||
* @param VEvent $vevent | |||||
* @return false|int|string | |||||
*/ | |||||
public function generateWhenString(VEvent $vevent) { | |||||
/** @var Property\ICalendar\DateTime $dtstart */ | |||||
$dtstart = $vevent->DTSTART; | |||||
if (isset($vevent->DTEND)) { | |||||
/** @var Property\ICalendar\DateTime $dtend */ | |||||
$dtend = $vevent->DTEND; | |||||
} elseif (isset($vevent->DURATION)) { | |||||
$isFloating = $dtstart->isFloating(); | |||||
$dtend = clone $dtstart; | |||||
$endDateTime = $dtend->getDateTime(); | |||||
$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue())); | |||||
$dtend->setDateTime($endDateTime, $isFloating); | |||||
} elseif (!$dtstart->hasTime()) { | |||||
$isFloating = $dtstart->isFloating(); | |||||
$dtend = clone $dtstart; | |||||
$endDateTime = $dtend->getDateTime(); | |||||
$endDateTime = $endDateTime->modify('+1 day'); | |||||
$dtend->setDateTime($endDateTime, $isFloating); | |||||
} else { | |||||
$dtend = clone $dtstart; | |||||
} | |||||
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */ | |||||
/** @var \DateTimeImmutable $dtstartDt */ | |||||
$dtstartDt = $dtstart->getDateTime(); | |||||
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */ | |||||
/** @var \DateTimeImmutable $dtendDt */ | |||||
$dtendDt = $dtend->getDateTime(); | |||||
$diff = $dtstartDt->diff($dtendDt); | |||||
$dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM)); | |||||
$dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM)); | |||||
if ($dtstart instanceof Property\ICalendar\Date) { | |||||
// One day event | |||||
if ($diff->days === 1) { | |||||
return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']); | |||||
} | |||||
// DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05, | |||||
// the email should show 2020-01-01 to 2020-01-04. | |||||
$dtendDt->modify('-1 day'); | |||||
//event that spans over multiple days | |||||
$localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']); | |||||
$localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']); | |||||
return $localeStart . ' - ' . $localeEnd; | |||||
} | |||||
/** @var Property\ICalendar\DateTime $dtstart */ | |||||
/** @var Property\ICalendar\DateTime $dtend */ | |||||
$isFloating = $dtstart->isFloating(); | |||||
$startTimezone = $endTimezone = null; | |||||
if (!$isFloating) { | |||||
$prop = $dtstart->offsetGet('TZID'); | |||||
if ($prop instanceof Parameter) { | |||||
$startTimezone = $prop->getValue(); | |||||
} | |||||
$prop = $dtend->offsetGet('TZID'); | |||||
if ($prop instanceof Parameter) { | |||||
$endTimezone = $prop->getValue(); | |||||
} | |||||
} | |||||
$localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' . | |||||
$this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']); | |||||
// always show full date with timezone if timezones are different | |||||
if ($startTimezone !== $endTimezone) { | |||||
$localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']); | |||||
return $localeStart . ' (' . $startTimezone . ') - ' . | |||||
$localeEnd . ' (' . $endTimezone . ')'; | |||||
} | |||||
// show only end time if date is the same | |||||
if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) { | |||||
$localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']); | |||||
} else { | |||||
$localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' . | |||||
$this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']); | |||||
} | |||||
return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')'; | |||||
} | |||||
/** | |||||
* @param VEvent $vEvent | |||||
* @return array | |||||
*/ | |||||
public function buildCancelledBodyData(VEvent $vEvent): array { | |||||
$defaultVal = ''; | |||||
$strikethrough = "<span style='text-decoration: line-through'>%s</span>"; | |||||
$newMeetingWhen = $this->generateWhenString($vEvent); | |||||
$newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');; | |||||
$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal; | |||||
$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal; | |||||
$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal; | |||||
$data = []; | |||||
$data['meeting_when_html'] = $newMeetingWhen === '' ?: sprintf($strikethrough, $newMeetingWhen); | |||||
$data['meeting_when'] = $newMeetingWhen; | |||||
$data['meeting_title_html'] = sprintf($strikethrough, $newSummary); | |||||
$data['meeting_title'] = $newSummary !== '' ? $newSummary: $this->l10n->t('Untitled event'); | |||||
$data['meeting_description_html'] = $newDescription !== '' ? sprintf($strikethrough, $newDescription) : ''; | |||||
$data['meeting_description'] = $newDescription; | |||||
$data['meeting_url_html'] = $newUrl !== '' ? sprintf($strikethrough, $newUrl) : ''; | |||||
$data['meeting_url'] = isset($vEvent->URL) ? (string)$vEvent->URL : ''; | |||||
$data['meeting_location_html'] = $newLocation !== '' ? sprintf($strikethrough, $newLocation) : ''; | |||||
$data['meeting_location'] = $newLocation; | |||||
return $data; | |||||
} | |||||
/** | |||||
* Check if event took place in the past | |||||
* | |||||
* @param VCalendar $vObject | |||||
* @return int | |||||
*/ | |||||
public function getLastOccurrence(VCalendar $vObject) { | |||||
/** @var VEvent $component */ | |||||
$component = $vObject->VEVENT; | |||||
if (isset($component->RRULE)) { | |||||
$it = new EventIterator($vObject, (string)$component->UID); | |||||
$maxDate = new \DateTime(IMipPlugin::MAX_DATE); | |||||
if ($it->isInfinite()) { | |||||
return $maxDate->getTimestamp(); | |||||
} | |||||
$end = $it->getDtEnd(); | |||||
while ($it->valid() && $end < $maxDate) { | |||||
$end = $it->getDtEnd(); | |||||
$it->next(); | |||||
} | |||||
return $end->getTimestamp(); | |||||
} | |||||
/** @var Property\ICalendar\DateTime $dtStart */ | |||||
$dtStart = $component->DTSTART; | |||||
if (isset($component->DTEND)) { | |||||
/** @var Property\ICalendar\DateTime $dtEnd */ | |||||
$dtEnd = $component->DTEND; | |||||
return $dtEnd->getDateTime()->getTimeStamp(); | |||||
} | |||||
if(isset($component->DURATION)) { | |||||
/** @var \DateTime $endDate */ | |||||
$endDate = clone $dtStart->getDateTime(); | |||||
// $component->DTEND->getDateTime() returns DateTimeImmutable | |||||
$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue())); | |||||
return $endDate->getTimestamp(); | |||||
} | |||||
if(!$dtStart->hasTime()) { | |||||
/** @var \DateTime $endDate */ | |||||
// $component->DTSTART->getDateTime() returns DateTimeImmutable | |||||
$endDate = clone $dtStart->getDateTime(); | |||||
$endDate = $endDate->modify('+1 day'); | |||||
return $endDate->getTimestamp(); | |||||
} | |||||
// No computation of end time possible - return start date | |||||
return $dtStart->getDateTime()->getTimeStamp(); | |||||
} | |||||
/** | |||||
* @param Property|null $attendee | |||||
*/ | |||||
public function setL10n(?Property $attendee = null) { | |||||
if($attendee === null) { | |||||
return; | |||||
} | |||||
$lang = $attendee->offsetGet('LANGUAGE'); | |||||
if ($lang instanceof Parameter) { | |||||
$lang = $lang->getValue(); | |||||
$this->l10n = $this->l10nFactory->get('dav', $lang); | |||||
} | |||||
} | |||||
/** | |||||
* @param Property|null $attendee | |||||
* @return bool | |||||
*/ | |||||
public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) { | |||||
if($attendee === null) { | |||||
return false; | |||||
} | |||||
$rsvp = $attendee->offsetGet('RSVP'); | |||||
if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) { | |||||
return true; | |||||
} | |||||
$role = $attendee->offsetGet('ROLE'); | |||||
// @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16 | |||||
// Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set | |||||
if ($role === null | |||||
|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0)) | |||||
|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0)) | |||||
) { | |||||
return true; | |||||
} | |||||
// RFC 5545 3.2.17: default RSVP is false | |||||
return false; | |||||
} | |||||
/** | |||||
* @param IEMailTemplate $template | |||||
* @param string $method | |||||
* @param string $sender | |||||
* @param string $summary | |||||
* @param string|null $partstat | |||||
*/ | |||||
public function addSubjectAndHeading(IEMailTemplate $template, | |||||
string $method, string $sender, string $summary): void { | |||||
if ($method === IMipPlugin::METHOD_CANCEL) { | |||||
// TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}" | |||||
$template->setSubject($this->l10n->t('Cancelled: %1$s', [$summary])); | |||||
$template->addHeading($this->l10n->t('"%1$s" has been canceled', [$summary])); | |||||
} elseif ($method === IMipPlugin::METHOD_REPLY) { | |||||
// TRANSLATORS Subject for email, when an invitation is replied to. Ex: "Re: {{Event Name}}" | |||||
$template->setSubject($this->l10n->t('Re: %1$s', [$summary])); | |||||
$template->addHeading($this->l10n->t('%1$s has responded your invitation', [$sender])); | |||||
} else { | |||||
// TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}" | |||||
$template->setSubject($this->l10n->t('Invitation: %1$s', [$summary])); | |||||
$template->addHeading($this->l10n->t('%1$s would like to invite you to "%2$s"', [$sender, $summary])); | |||||
} | |||||
} | |||||
/** | |||||
* @param string $path | |||||
* @return string | |||||
*/ | |||||
public function getAbsoluteImagePath($path): string { | |||||
return $this->urlGenerator->getAbsoluteURL( | |||||
$this->urlGenerator->imagePath('core', $path) | |||||
); | |||||
} | |||||
/** | |||||
* addAttendees: add organizer and attendee names/emails to iMip mail. | |||||
* | |||||
* Enable with DAV setting: invitation_list_attendees (default: no) | |||||
* | |||||
* The default is 'no', which matches old behavior, and is privacy preserving. | |||||
* | |||||
* To enable including attendees in invitation emails: | |||||
* % php occ config:app:set dav invitation_list_attendees --value yes | |||||
* | |||||
* @param IEMailTemplate $template | |||||
* @param IL10N $this->l10n | |||||
* @param VEvent $vevent | |||||
* @author brad2014 on github.com | |||||
*/ | |||||
public function addAttendees(IEMailTemplate $template, VEvent $vevent) { | |||||
if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') { | |||||
return; | |||||
} | |||||
if (isset($vevent->ORGANIZER)) { | |||||
/** @var Property | Property\ICalendar\CalAddress $organizer */ | |||||
$organizer = $vevent->ORGANIZER; | |||||
$organizerEmail = substr($organizer->getNormalizedValue(), 7); | |||||
/** @var string|null $organizerName */ | |||||
$organizerName = isset($organizer->CN) ? $organizer->CN->getValue() : null; | |||||
$organizerHTML = sprintf('<a href="%s">%s</a>', | |||||
htmlspecialchars($organizer->getNormalizedValue()), | |||||
htmlspecialchars($organizerName ?: $organizerEmail)); | |||||
$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail); | |||||
if(isset($organizer['PARTSTAT']) ) { | |||||
/** @var Parameter $partstat */ | |||||
$partstat = $organizer['PARTSTAT']; | |||||
if(strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) { | |||||
$organizerHTML .= ' ✔︎'; | |||||
$organizerText .= ' ✔︎'; | |||||
} | |||||
} | |||||
$template->addBodyListItem($organizerHTML, $this->l10n->t('Organizer:'), | |||||
$this->getAbsoluteImagePath('caldav/organizer.png'), | |||||
$organizerText, '', IMipPlugin::IMIP_INDENT); | |||||
} | |||||
$attendees = $vevent->select('ATTENDEE'); | |||||
if (count($attendees) === 0) { | |||||
return; | |||||
} | |||||
$attendeesHTML = []; | |||||
$attendeesText = []; | |||||
foreach ($attendees as $attendee) { | |||||
$attendeeEmail = substr($attendee->getNormalizedValue(), 7); | |||||
$attendeeName = isset($attendee['CN']) ? $attendee['CN']->getValue() : null; | |||||
$attendeeHTML = sprintf('<a href="%s">%s</a>', | |||||
htmlspecialchars($attendee->getNormalizedValue()), | |||||
htmlspecialchars($attendeeName ?: $attendeeEmail)); | |||||
$attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail); | |||||
if (isset($attendee['PARTSTAT'])) { | |||||
/** @var Parameter $partstat */ | |||||
$partstat = $attendee['PARTSTAT']; | |||||
if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) { | |||||
$attendeeHTML .= ' ✔︎'; | |||||
$attendeeText .= ' ✔︎'; | |||||
} | |||||
} | |||||
$attendeesHTML[] = $attendeeHTML; | |||||
$attendeesText[] = $attendeeText; | |||||
} | |||||
$template->addBodyListItem(implode('<br/>', $attendeesHTML), $this->l10n->t('Attendees:'), | |||||
$this->getAbsoluteImagePath('caldav/attendees.png'), | |||||
implode("\n", $attendeesText), '', IMipPlugin::IMIP_INDENT); | |||||
} | |||||
/** | |||||
* @param IEMailTemplate $template | |||||
* @param VEVENT $vevent | |||||
* @param $data | |||||
*/ | |||||
public function addBulletList(IEMailTemplate $template, VEvent $vevent, $data) { | |||||
$template->addBodyListItem( | |||||
$data['meeting_title'], $this->l10n->t('Title:'), | |||||
$this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT); | |||||
if ($data['meeting_when'] !== '') { | |||||
$template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Time:'), | |||||
$this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT); | |||||
} | |||||
if ($data['meeting_location'] !== '') { | |||||
$template->addBodyListItem($data['meeting_location_html'] ?? $data['meeting_location'], $this->l10n->t('Location:'), | |||||
$this->getAbsoluteImagePath('caldav/location.png'), $data['meeting_location'], '', IMipPlugin::IMIP_INDENT); | |||||
} | |||||
if ($data['meeting_url'] !== '') { | |||||
$template->addBodyListItem($data['meeting_url_html'] ?? $data['meeting_url'], $this->l10n->t('Link:'), | |||||
$this->getAbsoluteImagePath('caldav/link.png'), $data['meeting_url'], '', IMipPlugin::IMIP_INDENT); | |||||
} | |||||
$this->addAttendees($template, $vevent); | |||||
/* Put description last, like an email body, since it can be arbitrarily long */ | |||||
if ($data['meeting_description']) { | |||||
$template->addBodyListItem($data['meeting_description_html'] ?? $data['meeting_description'], $this->l10n->t('Description:'), | |||||
$this->getAbsoluteImagePath('caldav/description.png'), $data['meeting_description'], '', IMipPlugin::IMIP_INDENT); | |||||
} | |||||
} | |||||
/** | |||||
* @param Message $iTipMessage | |||||
* @return null|Property | |||||
*/ | |||||
public function getCurrentAttendee(Message $iTipMessage): ?Property { | |||||
/** @var VEvent $vevent */ | |||||
$vevent = $iTipMessage->message->VEVENT; | |||||
$attendees = $vevent->select('ATTENDEE'); | |||||
foreach ($attendees as $attendee) { | |||||
/** @var Property $attendee */ | |||||
if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) { | |||||
return $attendee; | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
/** | |||||
* @param Message $iTipMessage | |||||
* @param VEvent $vevent | |||||
* @param int $lastOccurrence | |||||
* @return string | |||||
*/ | |||||
public function createInvitationToken(Message $iTipMessage, VEvent $vevent, int $lastOccurrence): string { | |||||
$token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC); | |||||
$attendee = $iTipMessage->recipient; | |||||
$organizer = $iTipMessage->sender; | |||||
$sequence = $iTipMessage->sequence; | |||||
$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? | |||||
$vevent->{'RECURRENCE-ID'}->serialize() : null; | |||||
$uid = $vevent->{'UID'}; | |||||
$query = $this->db->getQueryBuilder(); | |||||
$query->insert('calendar_invitations') | |||||
->values([ | |||||
'token' => $query->createNamedParameter($token), | |||||
'attendee' => $query->createNamedParameter($attendee), | |||||
'organizer' => $query->createNamedParameter($organizer), | |||||
'sequence' => $query->createNamedParameter($sequence), | |||||
'recurrenceid' => $query->createNamedParameter($recurrenceId), | |||||
'expiration' => $query->createNamedParameter($lastOccurrence), | |||||
'uid' => $query->createNamedParameter($uid) | |||||
]) | |||||
->execute(); | |||||
return $token; | |||||
} | |||||
/** | |||||
* Create a valid VCalendar object out of the details of | |||||
* a VEvent and its associated iTip Message | |||||
* | |||||
* We do this to filter out all unchanged VEvents | |||||
* This is especially important in iTip Messages with recurrences | |||||
* and recurrence exceptions | |||||
* | |||||
* @param Message $iTipMessage | |||||
* @param VEvent $vEvent | |||||
* @return VCalendar | |||||
*/ | |||||
public function generateVCalendar(Message $iTipMessage, VEvent $vEvent): VCalendar { | |||||
$vCalendar = new VCalendar(); | |||||
$vCalendar->add('METHOD', $iTipMessage->method); | |||||
foreach ($iTipMessage->message->getComponents() as $component) { | |||||
if ($component instanceof VEvent) { | |||||
continue; | |||||
} | |||||
$vCalendar->add(clone $component); | |||||
} | |||||
$vCalendar->add($vEvent); | |||||
return $vCalendar; | |||||
} | |||||
/** | |||||
* @param IEMailTemplate $template | |||||
* @param $token | |||||
*/ | |||||
public function addResponseButtons(IEMailTemplate $template, $token) { | |||||
$template->addBodyButtonGroup( | |||||
$this->l10n->t('Accept'), | |||||
$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [ | |||||
'token' => $token, | |||||
]), | |||||
$this->l10n->t('Decline'), | |||||
$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [ | |||||
'token' => $token, | |||||
]) | |||||
); | |||||
} | |||||
public function addMoreOptionsButton(IEMailTemplate $template, $token) { | |||||
$moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [ | |||||
'token' => $token, | |||||
]); | |||||
$html = vsprintf('<small><a href="%s">%s</a></small>', [ | |||||
$moreOptionsURL, $this->l10n->t('More options …') | |||||
]); | |||||
$text = $this->l10n->t('More options at %s', [$moreOptionsURL]); | |||||
$template->addBodyText($html, $text); | |||||
} | |||||
} |
<?php | |||||
declare(strict_types=1); | |||||
/** | |||||
* @copyright 2023 Daniel Kesselberg <mail@danielkesselberg.de> | |||||
* | |||||
* @author 2023 Daniel Kesselberg <mail@danielkesselberg.de> | |||||
* | |||||
* @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/>. | |||||
* | |||||
*/ | |||||
namespace OCA\DAV\Tests\unit\CalDAV; | |||||
use OCA\DAV\CalDAV\EventComparisonService; | |||||
use Sabre\VObject\Component\VCalendar; | |||||
use Sabre\VObject\Component\VEvent; | |||||
use Test\TestCase; | |||||
class EventComparisonServiceTest extends TestCase | |||||
{ | |||||
/** @var EventComparisonService */ | |||||
private $eventComparisonService; | |||||
protected function setUp(): void | |||||
{ | |||||
$this->eventComparisonService = new EventComparisonService(); | |||||
} | |||||
public function testNoModifiedEvent(): void | |||||
{ | |||||
$vCalendarOld = new VCalendar(); | |||||
$vCalendarNew = new VCalendar(); | |||||
$vEventOld = $vCalendarOld->add('VEVENT', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', | |||||
]); | |||||
$vEventOld->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$vEventOld->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$vEventNew = $vCalendarNew->add('VEVENT', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', | |||||
]); | |||||
$vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld); | |||||
$this->assertEmpty($result['old']); | |||||
$this->assertEmpty($result['new']); | |||||
} | |||||
public function testNewEvent(): void | |||||
{ | |||||
$vCalendarOld = null; | |||||
$vCalendarNew = new VCalendar(); | |||||
$vEventNew = $vCalendarNew->add('VEVENT', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', | |||||
]); | |||||
$vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld); | |||||
$this->assertNull($result['old']); | |||||
$this->assertEquals([$vEventNew], $result['new']); | |||||
} | |||||
public function testModifiedUnmodifiedEvent(): void | |||||
{ | |||||
$vCalendarOld = new VCalendar(); | |||||
$vCalendarNew = new VCalendar(); | |||||
$vEventOld1 = $vCalendarOld->add('VEVENT', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
]); | |||||
$vEventOld1->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$vEventOld1->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$vEventOld2 = $vCalendarOld->add('VEVENT', [ | |||||
'UID' => 'uid-1235', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
]); | |||||
$vEventOld2->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$vEventOld2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$vEventNew1 = $vCalendarNew->add('VEVENT', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
]); | |||||
$vEventNew1->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$vEventNew1->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$vEventNew2 = $vCalendarNew->add('VEVENT', [ | |||||
'UID' => 'uid-1235', | |||||
'LAST-MODIFIED' => 123457, | |||||
'SEQUENCE' => 3, | |||||
'SUMMARY' => 'Fellowship meeting 2', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
]); | |||||
$vEventNew2->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$vEventNew2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld); | |||||
$this->assertEquals([$vEventOld2], $result['old']); | |||||
$this->assertEquals([$vEventNew2], $result['new']); | |||||
} | |||||
} |
*/ | */ | ||||
namespace OCA\DAV\Tests\unit\CalDAV\Schedule; | namespace OCA\DAV\Tests\unit\CalDAV\Schedule; | ||||
use OCA\DAV\CalDAV\EventComparisonService; | |||||
use OCA\DAV\CalDAV\Schedule\IMipPlugin; | use OCA\DAV\CalDAV\Schedule\IMipPlugin; | ||||
use OCA\DAV\CalDAV\Schedule\IMipService; | |||||
use OCP\AppFramework\Utility\ITimeFactory; | use OCP\AppFramework\Utility\ITimeFactory; | ||||
use OCP\DB\QueryBuilder\IQueryBuilder; | |||||
use OCP\Defaults; | use OCP\Defaults; | ||||
use OCP\IConfig; | use OCP\IConfig; | ||||
use OCP\IDBConnection; | |||||
use OCP\IL10N; | |||||
use OCP\IURLGenerator; | |||||
use OCP\IUserManager; | use OCP\IUserManager; | ||||
use OCP\L10N\IFactory; | |||||
use OCP\Mail\IAttachment; | use OCP\Mail\IAttachment; | ||||
use OCP\Mail\IEMailTemplate; | use OCP\Mail\IEMailTemplate; | ||||
use OCP\Mail\IMailer; | use OCP\Mail\IMailer; | ||||
use OCP\Mail\IMessage; | use OCP\Mail\IMessage; | ||||
use OCP\Security\ISecureRandom; | |||||
use PHPUnit\Framework\MockObject\MockObject; | use PHPUnit\Framework\MockObject\MockObject; | ||||
use Psr\Log\LoggerInterface; | use Psr\Log\LoggerInterface; | ||||
use Sabre\VObject\Component\VCalendar; | use Sabre\VObject\Component\VCalendar; | ||||
use Sabre\VObject\Component\VEvent; | |||||
use Sabre\VObject\ITip\Message; | use Sabre\VObject\ITip\Message; | ||||
use Test\TestCase; | use Test\TestCase; | ||||
use function array_merge; | |||||
class IMipPluginTest extends TestCase { | class IMipPluginTest extends TestCase { | ||||
/** @var IMessage|MockObject */ | /** @var IMessage|MockObject */ | ||||
private $mailMessage; | private $mailMessage; | ||||
/** @var IUserManager|MockObject */ | /** @var IUserManager|MockObject */ | ||||
private $userManager; | private $userManager; | ||||
/** @var IQueryBuilder|MockObject */ | |||||
private $queryBuilder; | |||||
/** @var IMipPlugin */ | /** @var IMipPlugin */ | ||||
private $plugin; | private $plugin; | ||||
/** @var IMipService|MockObject */ | |||||
private $service; | |||||
/** @var Defaults|MockObject */ | |||||
private $defaults; | |||||
/** @var LoggerInterface|MockObject */ | |||||
private $logger; | |||||
/** @var EventComparisonService|MockObject */ | |||||
private $eventComparisonService; | |||||
protected function setUp(): void { | protected function setUp(): void { | ||||
$this->mailMessage = $this->createMock(IMessage::class); | $this->mailMessage = $this->createMock(IMessage::class); | ||||
$this->mailMessage->method('setFrom')->willReturn($this->mailMessage); | $this->mailMessage->method('setFrom')->willReturn($this->mailMessage); | ||||
$this->mailMessage->method('setReplyTo')->willReturn($this->mailMessage); | $this->mailMessage->method('setReplyTo')->willReturn($this->mailMessage); | ||||
$this->mailMessage->method('setTo')->willReturn($this->mailMessage); | $this->mailMessage->method('setTo')->willReturn($this->mailMessage); | ||||
$this->mailer = $this->getMockBuilder(IMailer::class)->disableOriginalConstructor()->getMock(); | |||||
$this->mailer = $this->createMock(IMailer::class); | |||||
$this->mailer->method('createMessage')->willReturn($this->mailMessage); | $this->mailer->method('createMessage')->willReturn($this->mailMessage); | ||||
$this->emailTemplate = $this->createMock(IEMailTemplate::class); | $this->emailTemplate = $this->createMock(IEMailTemplate::class); | ||||
$this->emailAttachment = $this->createMock(IAttachment::class); | $this->emailAttachment = $this->createMock(IAttachment::class); | ||||
$this->mailer->method('createAttachment')->willReturn($this->emailAttachment); | $this->mailer->method('createAttachment')->willReturn($this->emailAttachment); | ||||
/** @var LoggerInterface|MockObject $logger */ | |||||
$logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock(); | |||||
$this->logger = $this->createMock(LoggerInterface::class); | |||||
$this->timeFactory = $this->getMockBuilder(ITimeFactory::class)->disableOriginalConstructor()->getMock(); | |||||
$this->timeFactory = $this->createMock(ITimeFactory::class); | |||||
$this->timeFactory->method('getTime')->willReturn(1496912528); // 2017-01-01 | $this->timeFactory->method('getTime')->willReturn(1496912528); // 2017-01-01 | ||||
$this->config = $this->createMock(IConfig::class); | $this->config = $this->createMock(IConfig::class); | ||||
$this->userManager = $this->createMock(IUserManager::class); | $this->userManager = $this->createMock(IUserManager::class); | ||||
$l10n = $this->createMock(IL10N::class); | |||||
$l10n->method('t') | |||||
->willReturnCallback(function ($text, $parameters = []) { | |||||
return vsprintf($text, $parameters); | |||||
}); | |||||
$l10nFactory = $this->createMock(IFactory::class); | |||||
$l10nFactory->method('get')->willReturn($l10n); | |||||
$urlGenerator = $this->createMock(IURLGenerator::class); | |||||
$this->queryBuilder = $this->createMock(IQueryBuilder::class); | |||||
$db = $this->createMock(IDBConnection::class); | |||||
$db->method('getQueryBuilder') | |||||
->with() | |||||
->willReturn($this->queryBuilder); | |||||
$random = $this->createMock(ISecureRandom::class); | |||||
$random->method('generate') | |||||
->with(60, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') | |||||
->willReturn('random_token'); | |||||
$defaults = $this->createMock(Defaults::class); | |||||
$defaults->method('getName') | |||||
$this->defaults = $this->createMock(Defaults::class); | |||||
$this->defaults->method('getName') | |||||
->willReturn('Instance Name 123'); | ->willReturn('Instance Name 123'); | ||||
$this->plugin = new IMipPlugin($this->config, $this->mailer, $logger, $this->timeFactory, $l10nFactory, $urlGenerator, $defaults, $random, $db, $this->userManager, 'user123'); | |||||
$this->service = $this->createMock(IMipService::class); | |||||
$this->eventComparisonService = $this->createMock(EventComparisonService::class); | |||||
$this->plugin = new IMipPlugin( | |||||
$this->config, | |||||
$this->mailer, | |||||
$this->logger, | |||||
$this->timeFactory, | |||||
$this->defaults, | |||||
$this->userManager, | |||||
'user123', | |||||
$this->service, | |||||
$this->eventComparisonService | |||||
); | |||||
} | } | ||||
public function testDelivery(): void { | |||||
$this->config | |||||
->expects($this->any()) | |||||
->method('getAppValue') | |||||
->willReturnMap([ | |||||
['dav', 'invitation_link_recipients', 'yes', 'yes'], | |||||
]); | |||||
$this->mailer->method('validateMailAddress')->willReturn(true); | |||||
$message = $this->_testMessage(); | |||||
$this->_expectSend(); | |||||
public function testDeliveryNoSignificantChange(): void { | |||||
$message = new Message(); | |||||
$message->method = 'REQUEST'; | |||||
$message->message = new VCalendar(); | |||||
$message->message->add('VEVENT', array_merge([ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 0, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00') | |||||
], [])); | |||||
$message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$message->message->VEVENT->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE']); | |||||
$message->sender = 'mailto:gandalf@wiz.ard'; | |||||
$message->senderName = 'Mr. Wizard'; | |||||
$message->recipient = 'mailto:' . 'frodo@hobb.it'; | |||||
$message->significantChange = false; | |||||
$this->plugin->schedule($message); | $this->plugin->schedule($message); | ||||
$this->assertEquals('1.1', $message->getScheduleStatus()); | |||||
$this->assertEquals('1.0', $message->getScheduleStatus()); | |||||
} | } | ||||
public function testFailedDelivery(): void { | |||||
$this->config | |||||
->expects($this->any()) | |||||
public function testParsingSingle(): void { | |||||
$message = new Message(); | |||||
$message->method = 'REQUEST'; | |||||
$newVCalendar = new VCalendar(); | |||||
$newVevent = new VEvent($newVCalendar, 'one', array_merge([ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 1, | |||||
'SUMMARY' => 'Fellowship meeting without (!) Boromir', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00') | |||||
], [])); | |||||
$newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$message->message = $newVCalendar; | |||||
$message->sender = 'mailto:gandalf@wiz.ard'; | |||||
$message->senderName = 'Mr. Wizard'; | |||||
$message->recipient = 'mailto:' . 'frodo@hobb.it'; | |||||
// save the old copy in the plugin | |||||
$oldVCalendar = new VCalendar(); | |||||
$oldVEvent = new VEvent($oldVCalendar, 'one', [ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 0, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00') | |||||
]); | |||||
$oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']); | |||||
$oldVCalendar->add($oldVEvent); | |||||
$data = ['invitee_name' => 'Mr. Wizard', | |||||
'meeting_title' => 'Fellowship meeting without (!) Boromir', | |||||
'attendee_name' => 'frodo@hobb.it' | |||||
]; | |||||
$this->plugin->setVCalendar($oldVCalendar); | |||||
$this->service->expects(self::once()) | |||||
->method('getLastOccurrence') | |||||
->willReturn('1496912700'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('validateMailAddress') | |||||
->with('frodo@hobb.it') | |||||
->willReturn(true); | |||||
$this->eventComparisonService->expects(self::once()) | |||||
->method('findModified') | |||||
->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]); | |||||
$this->service->expects(self::once()) | |||||
->method('buildBodyData') | |||||
->with($newVevent, $oldVEvent) | |||||
->willReturn($data); | |||||
$this->userManager->expects(self::never()) | |||||
->method('getDisplayName'); | |||||
$this->service->expects(self::once()) | |||||
->method('getFrom'); | |||||
$this->service->expects(self::once()) | |||||
->method('addSubjectAndHeading') | |||||
->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir'); | |||||
$this->service->expects(self::once()) | |||||
->method('addBulletList') | |||||
->with($this->emailTemplate, $newVevent, $data); | |||||
$this->service->expects(self::once()) | |||||
->method('getAttendeeRsvpOrReqForParticipant') | |||||
->willReturn(true); | |||||
$this->config->expects(self::once()) | |||||
->method('getAppValue') | ->method('getAppValue') | ||||
->willReturnMap([ | |||||
['dav', 'invitation_link_recipients', 'yes', 'yes'], | |||||
]); | |||||
$this->mailer->method('validateMailAddress')->willReturn(true); | |||||
$message = $this->_testMessage(); | |||||
$this->mailer | |||||
->with('dav', 'invitation_link_recipients', 'yes') | |||||
->willReturn('yes'); | |||||
$this->service->expects(self::once()) | |||||
->method('createInvitationToken') | |||||
->with($message,$newVevent, '1496912700') | |||||
->willReturn('token'); | |||||
$this->service->expects(self::once()) | |||||
->method('addResponseButtons') | |||||
->with($this->emailTemplate, 'token'); | |||||
$this->service->expects(self::once()) | |||||
->method('addMoreOptionsButton') | |||||
->with($this->emailTemplate, 'token'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('send') | ->method('send') | ||||
->willThrowException(new \Exception()); | |||||
$this->_expectSend(); | |||||
$this->plugin->schedule($message); | |||||
$this->assertEquals('5.0', $message->getScheduleStatus()); | |||||
} | |||||
public function testInvalidEmailDelivery(): void { | |||||
$this->mailer->method('validateMailAddress')->willReturn(false); | |||||
$message = $this->_testMessage(); | |||||
->willReturn([]); | |||||
$this->plugin->schedule($message); | $this->plugin->schedule($message); | ||||
$this->assertEquals('5.0', $message->getScheduleStatus()); | |||||
$this->assertEquals('1.1', $message->getScheduleStatus()); | |||||
} | } | ||||
public function testDeliveryWithNoCommonName(): void { | |||||
$this->config | |||||
->expects($this->any()) | |||||
->method('getAppValue') | |||||
->willReturnMap([ | |||||
['dav', 'invitation_link_recipients', 'yes', 'yes'], | |||||
]); | |||||
$this->mailer->method('validateMailAddress')->willReturn(true); | |||||
$message = $this->_testMessage(); | |||||
$message->senderName = null; | |||||
$this->userManager->expects($this->once()) | |||||
public function testParsingRecurrence(): void { | |||||
$message = new Message(); | |||||
$message->method = 'REQUEST'; | |||||
$newVCalendar = new VCalendar(); | |||||
$newVevent = new VEvent($newVCalendar, 'one', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z' | |||||
]); | |||||
$newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$newvEvent2 = new VEvent($newVCalendar, 'two', [ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 1, | |||||
'SUMMARY' => 'Elevenses', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00') | |||||
]); | |||||
$newvEvent2->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$newvEvent2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$message->message = $newVCalendar; | |||||
$message->sender = 'mailto:gandalf@wiz.ard'; | |||||
$message->recipient = 'mailto:' . 'frodo@hobb.it'; | |||||
// save the old copy in the plugin | |||||
$oldVCalendar = new VCalendar(); | |||||
$oldVEvent = new VEvent($oldVCalendar, 'one', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z' | |||||
]); | |||||
$oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$data = ['invitee_name' => 'Mr. Wizard', | |||||
'meeting_title' => 'Elevenses', | |||||
'attendee_name' => 'frodo@hobb.it' | |||||
]; | |||||
$this->plugin->setVCalendar($oldVCalendar); | |||||
$this->service->expects(self::once()) | |||||
->method('getLastOccurrence') | |||||
->willReturn('1496912700'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('validateMailAddress') | |||||
->with('frodo@hobb.it') | |||||
->willReturn(true); | |||||
$this->eventComparisonService->expects(self::once()) | |||||
->method('findModified') | |||||
->willReturn(['old' => [] ,'new' => [$newVevent]]); | |||||
$this->service->expects(self::once()) | |||||
->method('buildBodyData') | |||||
->with($newVevent, null) | |||||
->willReturn($data); | |||||
$this->userManager->expects(self::once()) | |||||
->method('getDisplayName') | ->method('getDisplayName') | ||||
->with('user123') | |||||
->willReturn('Mr. Wizard'); | ->willReturn('Mr. Wizard'); | ||||
$this->_expectSend(); | |||||
$this->service->expects(self::once()) | |||||
->method('getFrom'); | |||||
$this->service->expects(self::once()) | |||||
->method('addSubjectAndHeading') | |||||
->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Elevenses'); | |||||
$this->service->expects(self::once()) | |||||
->method('addBulletList') | |||||
->with($this->emailTemplate, $newVevent, $data); | |||||
$this->service->expects(self::once()) | |||||
->method('getAttendeeRsvpOrReqForParticipant') | |||||
->willReturn(true); | |||||
$this->config->expects(self::once()) | |||||
->method('getAppValue') | |||||
->with('dav', 'invitation_link_recipients', 'yes') | |||||
->willReturn('yes'); | |||||
$this->service->expects(self::once()) | |||||
->method('createInvitationToken') | |||||
->with($message, $newVevent, '1496912700') | |||||
->willReturn('token'); | |||||
$this->service->expects(self::once()) | |||||
->method('addResponseButtons') | |||||
->with($this->emailTemplate, 'token'); | |||||
$this->service->expects(self::once()) | |||||
->method('addMoreOptionsButton') | |||||
->with($this->emailTemplate, 'token'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('send') | |||||
->willReturn([]); | |||||
$this->plugin->schedule($message); | $this->plugin->schedule($message); | ||||
$this->assertEquals('1.1', $message->getScheduleStatus()); | $this->assertEquals('1.1', $message->getScheduleStatus()); | ||||
} | } | ||||
/** | |||||
* @dataProvider dataNoMessageSendForPastEvents | |||||
*/ | |||||
public function testNoMessageSendForPastEvents(array $veventParams, bool $expectsMail): void { | |||||
$this->config | |||||
->method('getAppValue') | |||||
->willReturn('yes'); | |||||
$this->mailer->method('validateMailAddress')->willReturn(true); | |||||
$message = $this->_testMessage($veventParams); | |||||
public function testEmailValidationFailed() { | |||||
$message = new Message(); | |||||
$message->method = 'REQUEST'; | |||||
$message->message = new VCalendar(); | |||||
$message->message->add('VEVENT', array_merge([ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 0, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00') | |||||
], [])); | |||||
$message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$message->message->VEVENT->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE']); | |||||
$message->sender = 'mailto:gandalf@wiz.ard'; | |||||
$message->senderName = 'Mr. Wizard'; | |||||
$message->recipient = 'mailto:' . 'frodo@hobb.it'; | |||||
$this->_expectSend('frodo@hobb.it', $expectsMail, $expectsMail); | |||||
$this->service->expects(self::once()) | |||||
->method('getLastOccurrence') | |||||
->willReturn('1496912700'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('validateMailAddress') | |||||
->with('frodo@hobb.it') | |||||
->willReturn(false); | |||||
$this->plugin->schedule($message); | $this->plugin->schedule($message); | ||||
if ($expectsMail) { | |||||
$this->assertEquals('1.1', $message->getScheduleStatus()); | |||||
} else { | |||||
$this->assertEquals(false, $message->getScheduleStatus()); | |||||
} | |||||
$this->assertEquals('5.0', $message->getScheduleStatus()); | |||||
} | } | ||||
public function dataNoMessageSendForPastEvents() { | |||||
return [ | |||||
[['DTSTART' => new \DateTime('2017-01-01 00:00:00')], false], | |||||
[['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00')], false], | |||||
[['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-12-31 00:00:00')], true], | |||||
[['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DURATION' => 'P1D'], false], | |||||
[['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DURATION' => 'P52W'], true], | |||||
[['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00'), 'RRULE' => 'FREQ=WEEKLY'], true], | |||||
[['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00'), 'RRULE' => 'FREQ=WEEKLY;COUNT=3'], false], | |||||
[['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00'), 'RRULE' => 'FREQ=WEEKLY;UNTIL=20170301T000000Z'], false], | |||||
[['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00'), 'RRULE' => 'FREQ=WEEKLY;COUNT=33'], true], | |||||
[['DTSTART' => new \DateTime('2017-01-01 00:00:00'), 'DTEND' => new \DateTime('2017-01-01 00:00:00'), 'RRULE' => 'FREQ=WEEKLY;UNTIL=20171001T000000Z'], true], | |||||
public function testFailedDelivery(): void { | |||||
$message = new Message(); | |||||
$message->method = 'REQUEST'; | |||||
$newVcalendar = new VCalendar(); | |||||
$newVevent = new VEvent($newVcalendar, 'one', array_merge([ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 1, | |||||
'SUMMARY' => 'Fellowship meeting without (!) Boromir', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00') | |||||
], [])); | |||||
$newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$message->message = $newVcalendar; | |||||
$message->sender = 'mailto:gandalf@wiz.ard'; | |||||
$message->senderName = 'Mr. Wizard'; | |||||
$message->recipient = 'mailto:' . 'frodo@hobb.it'; | |||||
// save the old copy in the plugin | |||||
$oldVcalendar = new VCalendar(); | |||||
$oldVevent = new VEvent($oldVcalendar, 'one', [ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 0, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00') | |||||
]); | |||||
$oldVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$oldVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$oldVevent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']); | |||||
$oldVcalendar->add($oldVevent); | |||||
$data = ['invitee_name' => 'Mr. Wizard', | |||||
'meeting_title' => 'Fellowship meeting without (!) Boromir', | |||||
'attendee_name' => 'frodo@hobb.it' | |||||
]; | ]; | ||||
} | |||||
/** | |||||
* @dataProvider dataIncludeResponseButtons | |||||
*/ | |||||
public function testIncludeResponseButtons(string $config_setting, string $recipient, bool $has_buttons): void { | |||||
$message = $this->_testMessage([], $recipient); | |||||
$this->mailer->method('validateMailAddress')->willReturn(true); | |||||
$this->_expectSend($recipient, true, $has_buttons); | |||||
$this->config | |||||
->expects($this->any()) | |||||
$this->plugin->setVCalendar($oldVcalendar); | |||||
$this->service->expects(self::once()) | |||||
->method('getLastOccurrence') | |||||
->willReturn('1496912700'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('validateMailAddress') | |||||
->with('frodo@hobb.it') | |||||
->willReturn(true); | |||||
$this->eventComparisonService->expects(self::once()) | |||||
->method('findModified') | |||||
->willReturn(['old' => [] ,'new' => [$newVevent]]); | |||||
$this->service->expects(self::once()) | |||||
->method('buildBodyData') | |||||
->with($newVevent, null) | |||||
->willReturn($data); | |||||
$this->userManager->expects(self::never()) | |||||
->method('getDisplayName'); | |||||
$this->service->expects(self::once()) | |||||
->method('getFrom'); | |||||
$this->service->expects(self::once()) | |||||
->method('addSubjectAndHeading') | |||||
->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir'); | |||||
$this->service->expects(self::once()) | |||||
->method('addBulletList') | |||||
->with($this->emailTemplate, $newVevent, $data); | |||||
$this->service->expects(self::once()) | |||||
->method('getAttendeeRsvpOrReqForParticipant') | |||||
->willReturn(true); | |||||
$this->config->expects(self::once()) | |||||
->method('getAppValue') | ->method('getAppValue') | ||||
->willReturnMap([ | |||||
['dav', 'invitation_link_recipients', 'yes', $config_setting], | |||||
]); | |||||
->with('dav', 'invitation_link_recipients', 'yes') | |||||
->willReturn('yes'); | |||||
$this->service->expects(self::once()) | |||||
->method('createInvitationToken') | |||||
->with($message, $newVevent, '1496912700') | |||||
->willReturn('token'); | |||||
$this->service->expects(self::once()) | |||||
->method('addResponseButtons') | |||||
->with($this->emailTemplate, 'token'); | |||||
$this->service->expects(self::once()) | |||||
->method('addMoreOptionsButton') | |||||
->with($this->emailTemplate, 'token'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('send') | |||||
->willReturn([]); | |||||
$this->mailer | |||||
->method('send') | |||||
->willThrowException(new \Exception()); | |||||
$this->logger->expects(self::once()) | |||||
->method('error'); | |||||
$this->plugin->schedule($message); | $this->plugin->schedule($message); | ||||
$this->assertEquals('1.1', $message->getScheduleStatus()); | |||||
$this->assertEquals('5.0', $message->getScheduleStatus()); | |||||
} | } | ||||
public function dataIncludeResponseButtons() { | |||||
return [ | |||||
// dav.invitation_link_recipients, recipient, $has_buttons | |||||
[ 'yes', 'joe@internal.com', true], | |||||
[ 'joe@internal.com', 'joe@internal.com', true], | |||||
[ 'internal.com', 'joe@internal.com', true], | |||||
[ 'pete@otherinternal.com,internal.com', 'joe@internal.com', true], | |||||
[ 'no', 'joe@internal.com', false], | |||||
[ 'internal.com', 'joe@external.com', false], | |||||
[ 'jane@otherinternal.com,internal.com', 'joe@otherinternal.com', false], | |||||
public function testNoOldEvent(): void { | |||||
$message = new Message(); | |||||
$message->method = 'REQUEST'; | |||||
$newVCalendar = new VCalendar(); | |||||
$newVevent = new VEvent($newVCalendar, 'VEVENT', array_merge([ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 1, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00') | |||||
], [])); | |||||
$newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$message->message = $newVCalendar; | |||||
$message->sender = 'mailto:gandalf@wiz.ard'; | |||||
$message->senderName = 'Mr. Wizard'; | |||||
$message->recipient = 'mailto:' . 'frodo@hobb.it'; | |||||
$data = ['invitee_name' => 'Mr. Wizard', | |||||
'meeting_title' => 'Fellowship meeting', | |||||
'attendee_name' => 'frodo@hobb.it' | |||||
]; | ]; | ||||
} | |||||
public function testMessageSendWhenEventWithoutName(): void { | |||||
$this->config | |||||
$this->service->expects(self::once()) | |||||
->method('getLastOccurrence') | |||||
->willReturn('1496912700'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('validateMailAddress') | |||||
->with('frodo@hobb.it') | |||||
->willReturn(true); | |||||
$this->eventComparisonService->expects(self::once()) | |||||
->method('findModified') | |||||
->with($newVCalendar, null) | |||||
->willReturn(['old' => [] ,'new' => [$newVevent]]); | |||||
$this->service->expects(self::once()) | |||||
->method('buildBodyData') | |||||
->with($newVevent, null) | |||||
->willReturn($data); | |||||
$this->userManager->expects(self::never()) | |||||
->method('getDisplayName'); | |||||
$this->service->expects(self::once()) | |||||
->method('getFrom'); | |||||
$this->service->expects(self::once()) | |||||
->method('addSubjectAndHeading') | |||||
->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting'); | |||||
$this->service->expects(self::once()) | |||||
->method('addBulletList') | |||||
->with($this->emailTemplate, $newVevent, $data); | |||||
$this->service->expects(self::once()) | |||||
->method('getAttendeeRsvpOrReqForParticipant') | |||||
->willReturn(true); | |||||
$this->config->expects(self::once()) | |||||
->method('getAppValue') | ->method('getAppValue') | ||||
->with('dav', 'invitation_link_recipients', 'yes') | |||||
->willReturn('yes'); | ->willReturn('yes'); | ||||
$this->mailer->method('validateMailAddress')->willReturn(true); | |||||
$message = $this->_testMessage(['SUMMARY' => '']); | |||||
$this->_expectSend('frodo@hobb.it', true, true, 'Invitation: Untitled event'); | |||||
$this->emailTemplate->expects($this->once()) | |||||
->method('addHeading') | |||||
->with('Invitation'); | |||||
$this->service->expects(self::once()) | |||||
->method('createInvitationToken') | |||||
->with($message, $newVevent, '1496912700') | |||||
->willReturn('token'); | |||||
$this->service->expects(self::once()) | |||||
->method('addResponseButtons') | |||||
->with($this->emailTemplate, 'token'); | |||||
$this->service->expects(self::once()) | |||||
->method('addMoreOptionsButton') | |||||
->with($this->emailTemplate, 'token'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('send') | |||||
->willReturn([]); | |||||
$this->mailer | |||||
->method('send') | |||||
->willReturn([]); | |||||
$this->plugin->schedule($message); | $this->plugin->schedule($message); | ||||
$this->assertEquals('1.1', $message->getScheduleStatus()); | $this->assertEquals('1.1', $message->getScheduleStatus()); | ||||
} | } | ||||
private function _testMessage(array $attrs = [], string $recipient = 'frodo@hobb.it') { | |||||
public function testNoButtons(): void { | |||||
$message = new Message(); | $message = new Message(); | ||||
$message->method = 'REQUEST'; | $message->method = 'REQUEST'; | ||||
$message->message = new VCalendar(); | |||||
$message->message->add('VEVENT', array_merge([ | |||||
$newVCalendar = new VCalendar(); | |||||
$newVevent = new VEvent($newVCalendar, 'VEVENT', array_merge([ | |||||
'UID' => 'uid-1234', | 'UID' => 'uid-1234', | ||||
'SEQUENCE' => 0, | |||||
'SEQUENCE' => 1, | |||||
'SUMMARY' => 'Fellowship meeting', | 'SUMMARY' => 'Fellowship meeting', | ||||
'DTSTART' => new \DateTime('2018-01-01 00:00:00') | |||||
], $attrs)); | |||||
$message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$message->message->VEVENT->add('ATTENDEE', 'mailto:'.$recipient, [ 'RSVP' => 'TRUE' ]); | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00') | |||||
], [])); | |||||
$newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$message->message = $newVCalendar; | |||||
$message->sender = 'mailto:gandalf@wiz.ard'; | $message->sender = 'mailto:gandalf@wiz.ard'; | ||||
$message->senderName = 'Mr. Wizard'; | |||||
$message->recipient = 'mailto:'.$recipient; | |||||
return $message; | |||||
} | |||||
$message->recipient = 'mailto:' . 'frodo@hobb.it'; | |||||
$data = ['invitee_name' => 'Mr. Wizard', | |||||
'meeting_title' => 'Fellowship meeting', | |||||
'attendee_name' => 'frodo@hobb.it' | |||||
]; | |||||
private function _expectSend(string $recipient = 'frodo@hobb.it', bool $expectSend = true, bool $expectButtons = true, string $subject = 'Invitation: Fellowship meeting'): void { | |||||
// if the event is in the past, we skip out | |||||
if (!$expectSend) { | |||||
$this->mailer | |||||
->expects($this->never()) | |||||
->method('send'); | |||||
return; | |||||
} | |||||
$this->emailTemplate->expects($this->once()) | |||||
->method('setSubject') | |||||
->with($subject); | |||||
$this->mailMessage->expects($this->once()) | |||||
->method('setTo') | |||||
->with([$recipient => null]); | |||||
$this->mailMessage->expects($this->once()) | |||||
->method('setReplyTo') | |||||
->with(['gandalf@wiz.ard' => 'Mr. Wizard']); | |||||
$this->mailMessage->expects($this->once()) | |||||
->method('setFrom') | |||||
->with(['invitations-noreply@localhost' => 'Mr. Wizard via Instance Name 123']); | |||||
$this->service->expects(self::once()) | |||||
->method('getLastOccurrence') | |||||
->willReturn('1496912700'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('validateMailAddress') | |||||
->with('frodo@hobb.it') | |||||
->willReturn(true); | |||||
$this->eventComparisonService->expects(self::once()) | |||||
->method('findModified') | |||||
->with($newVCalendar, null) | |||||
->willReturn(['old' => [] ,'new' => [$newVevent]]); | |||||
$this->service->expects(self::once()) | |||||
->method('buildBodyData') | |||||
->with($newVevent, null) | |||||
->willReturn($data); | |||||
$this->userManager->expects(self::once()) | |||||
->method('getDisplayName') | |||||
->willReturn('Mr. Wizard'); | |||||
$this->service->expects(self::once()) | |||||
->method('getFrom'); | |||||
$this->service->expects(self::once()) | |||||
->method('addSubjectAndHeading') | |||||
->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting'); | |||||
$this->service->expects(self::once()) | |||||
->method('addBulletList') | |||||
->with($this->emailTemplate, $newVevent, $data); | |||||
$this->service->expects(self::once()) | |||||
->method('getAttendeeRsvpOrReqForParticipant') | |||||
->willReturn(true); | |||||
$this->config->expects(self::once()) | |||||
->method('getAppValue') | |||||
->with('dav', 'invitation_link_recipients', 'yes') | |||||
->willReturn('no'); | |||||
$this->service->expects(self::never()) | |||||
->method('createInvitationToken'); | |||||
$this->service->expects(self::never()) | |||||
->method('addResponseButtons'); | |||||
$this->service->expects(self::never()) | |||||
->method('addMoreOptionsButton'); | |||||
$this->mailer->expects(self::once()) | |||||
->method('send') | |||||
->willReturn([]); | |||||
$this->mailer | $this->mailer | ||||
->expects($this->once()) | |||||
->method('send'); | |||||
if ($expectButtons) { | |||||
$this->queryBuilder->expects($this->once()) | |||||
->method('insert') | |||||
->with('calendar_invitations') | |||||
->willReturn($this->queryBuilder); | |||||
$this->queryBuilder->expects($this->once()) | |||||
->method('values') | |||||
->willReturn($this->queryBuilder); | |||||
$this->queryBuilder->expects($this->once()) | |||||
->method('execute'); | |||||
} else { | |||||
$this->queryBuilder->expects($this->never()) | |||||
->method('insert') | |||||
->with('calendar_invitations'); | |||||
} | |||||
->method('send') | |||||
->willReturn([]); | |||||
$this->plugin->schedule($message); | |||||
$this->assertEquals('1.1', $message->getScheduleStatus()); | |||||
} | } | ||||
} | } |
<?php | |||||
/** | |||||
* @copyright Copyright (c) 2016, ownCloud, Inc. | |||||
* @copyright Copyright (c) 2017, Georg Ehrke | |||||
* | |||||
* @author brad2014 <brad2014@users.noreply.github.com> | |||||
* @author Brad Rubenstein <brad@wbr.tech> | |||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at> | |||||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||||
* @author Joas Schilling <coding@schilljs.com> | |||||
* @author Morris Jobke <hey@morrisjobke.de> | |||||
* @author Thomas Citharel <nextcloud@tcit.fr> | |||||
* @author Thomas Müller <thomas.mueller@tmit.eu> | |||||
* | |||||
* @license AGPL-3.0 | |||||
* | |||||
* This code is free software: you can redistribute it and/or modify | |||||
* it under the terms of the GNU Affero General Public License, version 3, | |||||
* as published by the Free Software Foundation. | |||||
* | |||||
* 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, version 3, | |||||
* along with this program. If not, see <http://www.gnu.org/licenses/> | |||||
* | |||||
*/ | |||||
namespace OCA\DAV\Tests\unit\CalDAV\Schedule; | |||||
use OC\L10N\L10N; | |||||
use OC\L10N\LazyL10N; | |||||
use OC\URLGenerator; | |||||
use OCA\DAV\CalDAV\Schedule\IMipService; | |||||
use OCP\IConfig; | |||||
use OCP\IDBConnection; | |||||
use OCP\L10N\IFactory as L10NFactory; | |||||
use OCP\Security\ISecureRandom; | |||||
use PHPUnit\Framework\MockObject\MockObject; | |||||
use Sabre\VObject\Component\VCalendar; | |||||
use Sabre\VObject\Component\VEvent; | |||||
use Sabre\VObject\Property\ICalendar\DateTime; | |||||
use Test\TestCase; | |||||
class IMipServiceTest extends TestCase | |||||
{ | |||||
/** @var URLGenerator|MockObject */ | |||||
private $urlGenerator; | |||||
/** @var IConfig|MockObject */ | |||||
private $config; | |||||
/** @var IDBConnection|MockObject */ | |||||
private $db; | |||||
/** @var ISecureRandom|MockObject */ | |||||
private $random; | |||||
/** @var L10NFactory|MockObject */ | |||||
private $l10nFactory; | |||||
/** @var L10N|MockObject */ | |||||
private $l10n; | |||||
/** @var IMipService */ | |||||
private $service; | |||||
protected function setUp(): void | |||||
{ | |||||
$this->urlGenerator = $this->createMock(URLGenerator::class); | |||||
$this->config = $this->createMock(IConfig::class); | |||||
$this->db = $this->createMock(IDBConnection::class); | |||||
$this->random = $this->createMock(ISecureRandom::class); | |||||
$this->l10nFactory = $this->createMock(L10NFactory::class); | |||||
$this->l10n = $this->createMock(LazyL10N::class); | |||||
$this->l10nFactory->expects(self::once()) | |||||
->method('findGenericLanguage') | |||||
->willReturn('en'); | |||||
$this->l10nFactory->expects(self::once()) | |||||
->method('get') | |||||
->with('dav', 'en') | |||||
->willReturn($this->l10n); | |||||
$this->service = new IMipService( | |||||
$this->urlGenerator, | |||||
$this->config, | |||||
$this->db, | |||||
$this->random, | |||||
$this->l10nFactory | |||||
); | |||||
} | |||||
public function testGetFrom(): void | |||||
{ | |||||
$senderName = "Detective McQueen"; | |||||
$default = "Twin Lakes Police Department - Darkside Division"; | |||||
$expected = "Detective McQueen via Twin Lakes Police Department - Darkside Division"; | |||||
$this->l10n->expects(self::once()) | |||||
->method('t') | |||||
->willReturn($expected); | |||||
$actual = $this->service->getFrom($senderName, $default); | |||||
$this->assertEquals($expected, $actual); | |||||
} | |||||
public function testBuildBodyDataCreated(): void | |||||
{ | |||||
$vCalendar = new VCalendar(); | |||||
$oldVevent = null; | |||||
$newVevent = new VEvent($vCalendar, 'two', [ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 3, | |||||
'LAST-MODIFIED' => 789456, | |||||
'SUMMARY' => 'Second Breakfast', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00') | |||||
]); | |||||
$expected = [ | |||||
'meeting_when' => $this->service->generateWhenString($newVevent), | |||||
'meeting_description' => '', | |||||
'meeting_title' => 'Second Breakfast', | |||||
'meeting_location' => '', | |||||
'meeting_url' => '', | |||||
'meeting_url_html' => '', | |||||
]; | |||||
$actual = $this->service->buildBodyData($newVevent, $oldVevent); | |||||
$this->assertEquals($expected, $actual); | |||||
} | |||||
public function testBuildBodyDataUpdate(): void | |||||
{ | |||||
$vCalendar = new VCalendar(); | |||||
$oldVevent = new VEvent($vCalendar, 'two', [ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 1, | |||||
'LAST-MODIFIED' => 456789, | |||||
'SUMMARY' => 'Elevenses', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00') | |||||
]); | |||||
$oldVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); | |||||
$oldVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); | |||||
$newVevent = new VEvent($vCalendar, 'two', [ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 3, | |||||
'LAST-MODIFIED' => 789456, | |||||
'SUMMARY' => 'Second Breakfast', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00') | |||||
]); | |||||
$expected = [ | |||||
'meeting_when' => $this->service->generateWhenString($newVevent), | |||||
'meeting_description' => '', | |||||
'meeting_title' => 'Second Breakfast', | |||||
'meeting_location' => '', | |||||
'meeting_url' => '', | |||||
'meeting_url_html' => '', | |||||
'meeting_when_html' => $this->service->generateWhenString($newVevent), | |||||
'meeting_title_html' => sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", 'Elevenses', 'Second Breakfast'), | |||||
'meeting_description_html' => '', | |||||
'meeting_location_html' => '' | |||||
]; | |||||
$actual = $this->service->buildBodyData($newVevent, $oldVevent); | |||||
$this->assertEquals($expected, $actual); | |||||
} | |||||
public function testGenerateWhenStringHourlyEvent(): void { | |||||
$vCalendar = new VCalendar(); | |||||
$vevent = new VEvent($vCalendar, 'two', [ | |||||
'UID' => 'uid-1234', | |||||
'SEQUENCE' => 1, | |||||
'LAST-MODIFIED' => 456789, | |||||
'SUMMARY' => 'Elevenses', | |||||
'TZID' => 'Europe/Vienna', | |||||
'DTSTART' => (new \DateTime('2016-01-01 08:00:00'))->setTimezone(new \DateTimeZone('Europe/Vienna')), | |||||
'DTEND' => (new \DateTime('2016-01-01 09:00:00'))->setTimezone(new \DateTimeZone('Europe/Vienna')), | |||||
]); | |||||
$this->l10n->expects(self::exactly(3)) | |||||
->method('l') | |||||
->withConsecutive( | |||||
['weekdayName', (new \DateTime('2016-01-01 08:00:00'))->setTimezone(new \DateTimeZone('Europe/Vienna')), ['width' => 'abbreviated']], | |||||
['datetime', (new \DateTime('2016-01-01 08:00:00'))->setTimezone(new \DateTimeZone('Europe/Vienna')), ['width' => 'medium|short']], | |||||
['time', (new \DateTime('2016-01-01 09:00:00'))->setTimezone(new \DateTimeZone('Europe/Vienna')), ['width' => 'short']] | |||||
)->willReturnOnConsecutiveCalls( | |||||
'Fr.', | |||||
'01.01. 08:00', | |||||
'09:00' | |||||
); | |||||
$expected = 'Fr., 01.01. 08:00 - 09:00 (Europe/Vienna)'; | |||||
$actual = $this->service->generateWhenString($vevent); | |||||
$this->assertEquals($expected, $actual); | |||||
} | |||||
public function testGetLastOccurrenceRRULE(): void | |||||
{ | |||||
$vCalendar = new VCalendar(); | |||||
$vCalendar->add('VEVENT', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', | |||||
]); | |||||
$occurrence = $this->service->getLastOccurrence($vCalendar); | |||||
$this->assertEquals(1454284800, $occurrence); | |||||
} | |||||
public function testGetLastOccurrenceEndDate(): void | |||||
{ | |||||
$vCalendar = new VCalendar(); | |||||
$vCalendar->add('VEVENT', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'DTEND' => new \DateTime('2017-01-01 00:00:00'), | |||||
]); | |||||
$occurrence = $this->service->getLastOccurrence($vCalendar); | |||||
$this->assertEquals(1483228800, $occurrence); | |||||
} | |||||
public function testGetLastOccurrenceDuration(): void | |||||
{ | |||||
$vCalendar = new VCalendar(); | |||||
$vCalendar->add('VEVENT', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
'DURATION' => 'P12W', | |||||
]); | |||||
$occurrence = $this->service->getLastOccurrence($vCalendar); | |||||
$this->assertEquals(1458864000, $occurrence); | |||||
} | |||||
public function testGetLastOccurrenceAllDay(): void | |||||
{ | |||||
$vCalendar = new VCalendar(); | |||||
$vEvent = $vCalendar->add('VEVENT', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
]); | |||||
// rewrite from DateTime to Date | |||||
$vEvent->DTSTART['VALUE'] = 'DATE'; | |||||
$occurrence = $this->service->getLastOccurrence($vCalendar); | |||||
$this->assertEquals(1451692800, $occurrence); | |||||
} | |||||
public function testGetLastOccurrenceFallback(): void | |||||
{ | |||||
$vCalendar = new VCalendar(); | |||||
$vCalendar->add('VEVENT', [ | |||||
'UID' => 'uid-1234', | |||||
'LAST-MODIFIED' => 123456, | |||||
'SEQUENCE' => 2, | |||||
'SUMMARY' => 'Fellowship meeting', | |||||
'DTSTART' => new \DateTime('2016-01-01 00:00:00'), | |||||
]); | |||||
$occurrence = $this->service->getLastOccurrence($vCalendar); | |||||
$this->assertEquals(1451606400, $occurrence); | |||||
} | |||||
} |