Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable4.5] fix: add VTIMEZONE to Appointments #5564

Merged
merged 1 commit into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions lib/Service/Appointments/BookingCalendarWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,20 @@ class BookingCalendarWriter {
/** @var IL10N */
private $l10n;

private TimezoneGenerator $timezoneGenerator;

public function __construct(IConfig $config,
IManager $manager,
IUserManager $userManager,
ISecureRandom $random,
IL10N $l10n) {
IL10N $l10n,
TimezoneGenerator $timezoneGenerator) {
$this->config = $config;
$this->manager = $manager;
$this->userManager = $userManager;
$this->random = $random;
$this->l10n = $l10n;
$this->timezoneGenerator = $timezoneGenerator;
}

private function secondsToIso8601Duration(int $secs): string {
Expand Down Expand Up @@ -97,7 +101,7 @@ public function write(AppointmentConfig $config,
DateTimeImmutable $start,
string $displayName,
string $email,
?string $description = null,
string $timezone, ?string $description = null,
?string $location = null) : string {
$calendar = current($this->manager->getCalendarsForPrincipal($config->getPrincipalUri(), [$config->getTargetCalendarUri()]));
if (!($calendar instanceof ICreateFromString)) {
Expand All @@ -121,6 +125,12 @@ public function write(AppointmentConfig $config,
]
]);

$end = $start->getTimestamp() + $config->getLength();
$tz = $this->timezoneGenerator->generateVTimezone($timezone, $start->getTimestamp(), $end);
if($tz) {
$vcalendar->add($tz);
}

if (!empty($description)) {
$vcalendar->VEVENT->add('DESCRIPTION', $description);
}
Expand Down Expand Up @@ -171,7 +181,6 @@ public function write(AppointmentConfig $config,
$vcalendar->VEVENT->add($alarm);
}


if ($config->getLocation() !== null) {
$vcalendar->VEVENT->add('LOCATION', $config->getLocation());
}
Expand Down Expand Up @@ -199,6 +208,10 @@ public function write(AppointmentConfig $config,
'DTEND' => $start
]
]);
$tz = $this->timezoneGenerator->generateVTimezone($timezone, $prepStart->getTimestamp(), $start->getTimestamp());
if($tz) {
$prepCalendar->add($tz);
}

$prepCalendar->VEVENT->add('RELATED-TO', $vcalendar->VEVENT->{'UID'});
$prepCalendar->VEVENT->add('RELTYPE', 'PARENT');
Expand Down Expand Up @@ -228,6 +241,11 @@ public function write(AppointmentConfig $config,
]
]);

$tz = $this->timezoneGenerator->generateVTimezone($timezone, $followupStart->getTimestamp(), $followUpEnd->getTimestamp());
if($tz) {
$followUpCalendar->add($tz);
}

$followUpCalendar->VEVENT->add('RELATED-TO', $vcalendar->VEVENT->{'UID'});
$followUpCalendar->VEVENT->add('RELTYPE', 'PARENT');
$followUpCalendar->VEVENT->add('X-NC-POST-APPOINTMENT', $config->getToken());
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/Appointments/BookingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,13 @@ public function confirmBooking(Booking $booking, AppointmentConfig $config): Boo
$startObj,
$booking->getDisplayName(),
$booking->getEmail(),
$booking->getTimezone(),
$booking->getDescription(),
$config->getCreateTalkRoom() ? $booking->getTalkUrl() : $config->getLocation(),
);
$booking->setConfirmed(true);
$this->bookingMapper->update($booking);


try {
$this->mailService->sendBookingInformationEmail($booking, $config, $calendar);
$this->mailService->sendOrganizerBookingInformationEmail($booking, $config, $calendar);
Expand Down
143 changes: 143 additions & 0 deletions lib/Service/Appointments/TimezoneGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php
/*
* *
* * calendar App
* *
* * @copyright 2023 Anna Larch <[email protected]>
* *
* * @author Anna Larch <[email protected]>
* *
* * 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/>.
* *
*
*/

declare(strict_types=1);

/*
* @copyright 2023 Anna Larch <[email protected]>
*
* @author 2023 Anna Larch <[email protected]>
*
* @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\Calendar\Service\Appointments;

use Sabre\VObject\Component;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\TimeZoneUtil;
use function max;
use function min;

class TimezoneGenerator {
/**
* Returns a VTIMEZONE component for a Olson timezone identifier
* with daylight transitions covering the given date range.
*
* @link https://gist.github.com/thomascube/47ff7d530244c669825736b10877a200
*
* @param string $timezone Timezone
* @param integer $from Unix timestamp with first date/time in this timezone
* @param integer $to Unix timestap with last date/time in this timezone
* @psalm-suppress NoValue
*
* @return null|VTimeZone A Sabre\VObject\Component object representing a VTIMEZONE definition
* or null if no timezone information is available
*/
public function generateVtimezone(string $timezone, int $from, int $to): ?VTimeZone {
try {
$tz = new \DateTimeZone($timezone);
} catch (\Exception $e) {
return null;
}

// get all transitions for one year back/ahead
$year = 86400 * 360;
$transitions = $tz->getTransitions($from - $year, $to + $year);

$vcalendar = new VCalendar();
$vtimezone = $vcalendar->createComponent('VTIMEZONE');
$vtimezone->TZID = $timezone;

$standard = $daylightStart = null;
foreach ($transitions as $i => $trans) {
$component = null;

// skip the first entry...
if ($i === 0) {
// ... but remember the offset for the next TZOFFSETFROM value
$tzfrom = $trans['offset'] / 3600;
continue;
}

// daylight saving time definition
if ($trans['isdst']) {
$daylightDefinition = $trans['ts'];
$daylightStart = $vcalendar->createComponent('DAYLIGHT');
$component = $daylightStart;
}
// standard time definition
else {
$standardDefinition = $trans['ts'];
$standard = $vcalendar->createComponent('STANDARD');
$component = $standard;
}

if ($component) {
$date = new \DateTime($trans['time']);
$offset = $trans['offset'] / 3600;

$component->DTSTART = $date->format('Ymd\THis');
$component->TZOFFSETFROM = sprintf('%s%02d%02d', $tzfrom >= 0 ? '+' : '-', abs(floor($tzfrom)), ($tzfrom - floor($tzfrom)) * 60);
$component->TZOFFSETTO = sprintf('%s%02d%02d', $offset >= 0 ? '+' : '-', abs(floor($offset)), ($offset - floor($offset)) * 60);

// add abbreviated timezone name if available
if (!empty($trans['abbr'])) {
$component->TZNAME = $trans['abbr'];
}

$tzfrom = $offset;
$vtimezone->add($component);
}

// we covered the entire date range
if ($standard && $daylightStart && min($standardDefinition, $daylightDefinition) < $from && max($standardDefinition, $daylightDefinition) > $to) {
break;
}
}

// add X-MICROSOFT-CDO-TZID if available
$microsoftExchangeMap = array_flip(TimeZoneUtil::$microsoftExchangeMap);
if (!empty($microsoftExchangeMap) && array_key_exists($tz->getName(), $microsoftExchangeMap)) {
$vtimezone->add('X-MICROSOFT-CDO-TZID', $microsoftExchangeMap[$tz->getName()]);
}

return $vtimezone;
}
}
3 changes: 3 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
<referencedClass name="OC\Security\CSP\ContentSecurityPolicyNonceManager" />
<referencedClass name="Psr\Http\Client\ClientExceptionInterface" />
<referencedClass name="Sabre\VObject\Component\VCalendar" />
<referencedClass name="Sabre\VObject\Component\VTimezone" />
<referencedClass name="Sabre\VObject\TimeZoneUtil" />
<referencedClass name="Symfony\Component\HttpFoundation\IpUtils" />
<referencedClass name="Symfony\Component\Console\Command\Command" />
<referencedClass name="Symfony\Component\Console\Input\InputArgument" />
Expand All @@ -45,6 +47,7 @@
<referencedClass name="Doctrine\DBAL\Schema\SchemaException" />
<referencedClass name="Doctrine\DBAL\Schema\Table" />
<referencedClass name="OC\Security\CSP\ContentSecurityPolicyNonceManager" />
<referencedClass name="Sabre\VObject\Component\VTimezone" />
<referencedClass name="Symfony\Component\Console\Output\OutputInterface" />
</errorLevel>
</UndefinedDocblockClass>
Expand Down
91 changes: 91 additions & 0 deletions tests/php/unit/Service/Appointments/TimezoneGeneratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);
/**
* Calendar App
*
* @author 2023 Anna Larch <[email protected]>
* @copyright 2023 Anna Larch <[email protected]>
*
* 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\Calendar\Tests\Unit\Service\Appointments;

use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Calendar\Service\Appointments\TimezoneGenerator;
use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\TimeZoneUtil;

class TimezoneGeneratorTest extends TestCase {

protected TimezoneGenerator $timezoneGenerator;
protected function setUp(): void {
$this->timezoneGenerator = new TimezoneGenerator();
}

/**
* @dataProvider providerDaylightSaving
*/
public function testWithDaylightSaving($timezone, $daytime, $standard, $msTimezoneId): void {
/** @var VTimeZone $generated */
$generated = $this->timezoneGenerator->generateVtimezone($timezone, 1640991600, 1672527600);

$this->assertEquals($timezone, $generated->TZID->getValue());
$this->assertNotNull($generated->DAYLIGHT);
$this->assertCount($daytime, $generated->DAYLIGHT->getIterator());
$this->assertNotNull($generated->STANDARD);
$this->assertCount($standard, $generated->STANDARD->getIterator());
$this->assertEquals($generated->{'X-MICROSOFT-CDO-TZID'}->getValue(), $msTimezoneId);
}

/**
* @dataProvider providerNoDaylightSaving
*/
public function testNoDaylightSaving($timezone, $daytime, $standard, $msTimezoneId): void {
/** @var VTimeZone $generated */
$generated = $this->timezoneGenerator->generateVtimezone($timezone, 1640991600, 1672527600);

$this->assertEquals($timezone, $generated->TZID->getValue());
$this->assertNull($generated->DAYLIGHT);
$this->assertNull($generated->STANDARD);
$this->assertEquals($generated->{'X-MICROSOFT-CDO-TZID'}->getValue(), $msTimezoneId);
}

public function testInvalid(): void {
/** @var VTimeZone $generated */
$generated = $this->timezoneGenerator->generateVtimezone('Nonsense', 1640991600, 1672527600);

$this->assertNull($generated);
}

public function providerDaylightSaving(): array {
$microsoftExchangeMap = array_flip(TimeZoneUtil::$microsoftExchangeMap);
return [
['Europe/Berlin', 3, 3, $microsoftExchangeMap['Europe/Berlin']],
['Europe/London', 3, 3, $microsoftExchangeMap['Europe/London']],
['Australia/Adelaide', 3, 3, $microsoftExchangeMap['Australia/Adelaide']],
];
}

public function providerNoDaylightSaving(): array {
$microsoftExchangeMap = array_flip(TimeZoneUtil::$microsoftExchangeMap);
return [
['Pacific/Midway', null, null, $microsoftExchangeMap['Pacific/Midway']],
['Asia/Singapore', null, null, $microsoftExchangeMap['Asia/Singapore']],
];
}


}
Loading