Browse Source
Merge pull request #35743 from nextcloud/fix/use-recurrence-for-imip-email
Merge pull request #35743 from nextcloud/fix/use-recurrence-for-imip-email
Include more information in iMIP email and show diff information on updating an eventpull/36409/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1711 additions and 703 deletions
-
2apps/dav/composer/composer/autoload_classmap.php
-
2apps/dav/composer/composer/autoload_static.php
-
123apps/dav/lib/CalDAV/EventComparisonService.php
-
611apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
-
597apps/dav/lib/CalDAV/Schedule/IMipService.php
-
146apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php
-
649apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
-
284apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php
@ -0,0 +1,123 @@ |
|||
<?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)]; |
|||
} |
|||
} |
|||
@ -0,0 +1,597 @@ |
|||
<?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); |
|||
} |
|||
} |
|||
@ -0,0 +1,146 @@ |
|||
<?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']); |
|||
} |
|||
} |
|||
@ -0,0 +1,284 @@ |
|||
<?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); |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue