mirror of
https://github.com/nextcloud/server.git
synced 2026-02-03 20:41:22 -05:00
Merge pull request #56784 from nextcloud/fix/calendar-subscription-memory-exhaustion
fix: calendar subscription memory exhaustion
This commit is contained in:
commit
75edec9d6c
7 changed files with 501 additions and 354 deletions
|
|
@ -1066,9 +1066,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
* @param int $calendarType
|
||||
* @return array
|
||||
*/
|
||||
public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
|
||||
public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR, array $fields = []):array {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->select(['id','uid', 'etag', 'uri', 'calendardata'])
|
||||
$query->select($fields ?: ['id', 'uid', 'etag', 'uri', 'calendardata'])
|
||||
->from('calendarobjects')
|
||||
->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
|
||||
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
|
||||
|
|
@ -1077,12 +1077,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
|
||||
$result = [];
|
||||
while (($row = $stmt->fetchAssociative()) !== false) {
|
||||
$result[$row['uid']] = [
|
||||
'id' => $row['id'],
|
||||
'etag' => $row['etag'],
|
||||
'uri' => $row['uri'],
|
||||
'calendardata' => $row['calendardata'],
|
||||
];
|
||||
$result[$row['uid']] = $row;
|
||||
}
|
||||
$stmt->closeCursor();
|
||||
|
||||
|
|
|
|||
|
|
@ -23,9 +23,6 @@ use Sabre\VObject\UUIDUtil;
|
|||
*/
|
||||
class ImportService {
|
||||
|
||||
/** @var resource */
|
||||
private $source;
|
||||
|
||||
public function __construct(
|
||||
private CalDavBackend $backend,
|
||||
) {
|
||||
|
|
@ -44,18 +41,15 @@ class ImportService {
|
|||
if (!is_resource($source)) {
|
||||
throw new InvalidArgumentException('Invalid import source must be a file resource');
|
||||
}
|
||||
|
||||
$this->source = $source;
|
||||
|
||||
switch ($options->getFormat()) {
|
||||
case 'ical':
|
||||
return $this->importProcess($calendar, $options, $this->importText(...));
|
||||
return $this->importProcess($source, $calendar, $options, $this->importText(...));
|
||||
break;
|
||||
case 'jcal':
|
||||
return $this->importProcess($calendar, $options, $this->importJson(...));
|
||||
return $this->importProcess($source, $calendar, $options, $this->importJson(...));
|
||||
break;
|
||||
case 'xcal':
|
||||
return $this->importProcess($calendar, $options, $this->importXml(...));
|
||||
return $this->importProcess($source, $calendar, $options, $this->importXml(...));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid import format');
|
||||
|
|
@ -65,10 +59,15 @@ class ImportService {
|
|||
/**
|
||||
* Generates object stream from a text formatted source (ical)
|
||||
*
|
||||
* @param resource $source
|
||||
*
|
||||
* @return Generator<\Sabre\VObject\Component\VCalendar>
|
||||
*/
|
||||
private function importText(): Generator {
|
||||
$importer = new TextImporter($this->source);
|
||||
public function importText($source): Generator {
|
||||
if (!is_resource($source)) {
|
||||
throw new InvalidArgumentException('Invalid import source must be a file resource');
|
||||
}
|
||||
$importer = new TextImporter($source);
|
||||
$structure = $importer->structure();
|
||||
$sObjectPrefix = $importer::OBJECT_PREFIX;
|
||||
$sObjectSuffix = $importer::OBJECT_SUFFIX;
|
||||
|
|
@ -113,10 +112,15 @@ class ImportService {
|
|||
/**
|
||||
* Generates object stream from a xml formatted source (xcal)
|
||||
*
|
||||
* @param resource $source
|
||||
*
|
||||
* @return Generator<\Sabre\VObject\Component\VCalendar>
|
||||
*/
|
||||
private function importXml(): Generator {
|
||||
$importer = new XmlImporter($this->source);
|
||||
public function importXml($source): Generator {
|
||||
if (!is_resource($source)) {
|
||||
throw new InvalidArgumentException('Invalid import source must be a file resource');
|
||||
}
|
||||
$importer = new XmlImporter($source);
|
||||
$structure = $importer->structure();
|
||||
$sObjectPrefix = $importer::OBJECT_PREFIX;
|
||||
$sObjectSuffix = $importer::OBJECT_SUFFIX;
|
||||
|
|
@ -155,11 +159,16 @@ class ImportService {
|
|||
/**
|
||||
* Generates object stream from a json formatted source (jcal)
|
||||
*
|
||||
* @param resource $source
|
||||
*
|
||||
* @return Generator<\Sabre\VObject\Component\VCalendar>
|
||||
*/
|
||||
private function importJson(): Generator {
|
||||
public function importJson($source): Generator {
|
||||
if (!is_resource($source)) {
|
||||
throw new InvalidArgumentException('Invalid import source must be a file resource');
|
||||
}
|
||||
/** @var VCALENDAR $importer */
|
||||
$importer = Reader::readJson($this->source);
|
||||
$importer = Reader::readJson($source);
|
||||
// calendar time zones
|
||||
$timezones = [];
|
||||
foreach ($importer->VTIMEZONE as $timezone) {
|
||||
|
|
@ -212,17 +221,18 @@ class ImportService {
|
|||
*
|
||||
* @since 32.0.0
|
||||
*
|
||||
* @param resource $source
|
||||
* @param CalendarImportOptions $options
|
||||
* @param callable $generator<CalendarImportOptions>: Generator<\Sabre\VObject\Component\VCalendar>
|
||||
*
|
||||
* @return array<string,array<string,string|array<string>>>
|
||||
*/
|
||||
public function importProcess(CalendarImpl $calendar, CalendarImportOptions $options, callable $generator): array {
|
||||
public function importProcess($source, CalendarImpl $calendar, CalendarImportOptions $options, callable $generator): array {
|
||||
$calendarId = $calendar->getKey();
|
||||
$calendarUri = $calendar->getUri();
|
||||
$principalUri = $calendar->getPrincipalUri();
|
||||
$outcome = [];
|
||||
foreach ($generator() as $vObject) {
|
||||
foreach ($generator($source) as $vObject) {
|
||||
$components = $vObject->getBaseComponents();
|
||||
// determine if the object has no base component types
|
||||
if (count($components) === 0) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ use OCP\Http\Client\IClientService;
|
|||
use OCP\Http\Client\LocalServerException;
|
||||
use OCP\IAppConfig;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\VObject\Reader;
|
||||
|
||||
class Connection {
|
||||
public function __construct(
|
||||
|
|
@ -26,8 +25,10 @@ class Connection {
|
|||
|
||||
/**
|
||||
* gets webcal feed from remote server
|
||||
*
|
||||
* @return array{data: resource, format: string}|null
|
||||
*/
|
||||
public function queryWebcalFeed(array $subscription): ?string {
|
||||
public function queryWebcalFeed(array $subscription): ?array {
|
||||
$subscriptionId = $subscription['id'];
|
||||
$url = $this->cleanURL($subscription['source']);
|
||||
if ($url === null) {
|
||||
|
|
@ -54,6 +55,7 @@ class Connection {
|
|||
'User-Agent' => $uaString,
|
||||
'Accept' => 'text/calendar, application/calendar+json, application/calendar+xml',
|
||||
],
|
||||
'stream' => true,
|
||||
];
|
||||
|
||||
$user = parse_url($subscription['source'], PHP_URL_USER);
|
||||
|
|
@ -77,42 +79,22 @@ class Connection {
|
|||
return null;
|
||||
}
|
||||
|
||||
$body = $response->getBody();
|
||||
|
||||
$contentType = $response->getHeader('Content-Type');
|
||||
$contentType = explode(';', $contentType, 2)[0];
|
||||
switch ($contentType) {
|
||||
case 'application/calendar+json':
|
||||
try {
|
||||
$jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
|
||||
} catch (Exception $ex) {
|
||||
// In case of a parsing error return null
|
||||
$this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
|
||||
return null;
|
||||
}
|
||||
return $jCalendar->serialize();
|
||||
|
||||
case 'application/calendar+xml':
|
||||
try {
|
||||
$xCalendar = Reader::readXML($body);
|
||||
} catch (Exception $ex) {
|
||||
// In case of a parsing error return null
|
||||
$this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
|
||||
return null;
|
||||
}
|
||||
return $xCalendar->serialize();
|
||||
$format = match ($contentType) {
|
||||
'application/calendar+json' => 'jcal',
|
||||
'application/calendar+xml' => 'xcal',
|
||||
default => 'ical',
|
||||
};
|
||||
|
||||
case 'text/calendar':
|
||||
default:
|
||||
try {
|
||||
$vCalendar = Reader::read($body);
|
||||
} catch (Exception $ex) {
|
||||
// In case of a parsing error return null
|
||||
$this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
|
||||
return null;
|
||||
}
|
||||
return $vCalendar->serialize();
|
||||
// With 'stream' => true, getBody() returns the underlying stream resource
|
||||
$stream = $response->getBody();
|
||||
if (!is_resource($stream)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ['data' => $stream, 'format' => $format];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,18 +9,14 @@ declare(strict_types=1);
|
|||
namespace OCA\DAV\CalDAV\WebcalCaching;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Import\ImportService;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Exception\BadRequest;
|
||||
use Sabre\DAV\Exception\Forbidden;
|
||||
use Sabre\DAV\PropPatch;
|
||||
use Sabre\VObject\Component;
|
||||
use Sabre\VObject\DateTimeParser;
|
||||
use Sabre\VObject\InvalidDataException;
|
||||
use Sabre\VObject\ParseException;
|
||||
use Sabre\VObject\Reader;
|
||||
use Sabre\VObject\Recur\NoInstancesException;
|
||||
use Sabre\VObject\Splitter\ICalendar;
|
||||
use Sabre\VObject\UUIDUtil;
|
||||
use function count;
|
||||
|
||||
|
|
@ -36,20 +32,20 @@ class RefreshWebcalService {
|
|||
private LoggerInterface $logger,
|
||||
private Connection $connection,
|
||||
private ITimeFactory $time,
|
||||
private ImportService $importService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function refreshSubscription(string $principalUri, string $uri) {
|
||||
$subscription = $this->getSubscription($principalUri, $uri);
|
||||
$mutations = [];
|
||||
if (!$subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the refresh rate if there is any
|
||||
if (!empty($subscription['{http://apple.com/ns/ical/}refreshrate'])) {
|
||||
// add the refresh interval to the lastmodified timestamp
|
||||
$refreshInterval = new \DateInterval($subscription['{http://apple.com/ns/ical/}refreshrate']);
|
||||
if (!empty($subscription[self::REFRESH_RATE])) {
|
||||
// add the refresh interval to the last modified timestamp
|
||||
$refreshInterval = new \DateInterval($subscription[self::REFRESH_RATE]);
|
||||
$updateTime = $this->time->getDateTime();
|
||||
$updateTime->setTimestamp($subscription['lastmodified'])->add($refreshInterval);
|
||||
if ($updateTime->getTimestamp() > $this->time->getTime()) {
|
||||
|
|
@ -57,109 +53,116 @@ class RefreshWebcalService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
$webcalData = $this->connection->queryWebcalFeed($subscription);
|
||||
if (!$webcalData) {
|
||||
$result = $this->connection->queryWebcalFeed($subscription);
|
||||
if (!$result) {
|
||||
return;
|
||||
}
|
||||
|
||||
$localData = $this->calDavBackend->getLimitedCalendarObjects((int)$subscription['id'], CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
|
||||
$data = $result['data'];
|
||||
$format = $result['format'];
|
||||
|
||||
$stripTodos = ($subscription[self::STRIP_TODOS] ?? 1) === 1;
|
||||
$stripAlarms = ($subscription[self::STRIP_ALARMS] ?? 1) === 1;
|
||||
$stripAttachments = ($subscription[self::STRIP_ATTACHMENTS] ?? 1) === 1;
|
||||
|
||||
try {
|
||||
$splitter = new ICalendar($webcalData, Reader::OPTION_FORGIVING);
|
||||
$existingObjects = $this->calDavBackend->getLimitedCalendarObjects((int)$subscription['id'], CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION, ['id', 'uid', 'etag', 'uri']);
|
||||
|
||||
while ($vObject = $splitter->getNext()) {
|
||||
/** @var Component $vObject */
|
||||
$compName = null;
|
||||
$uid = null;
|
||||
$generator = match ($format) {
|
||||
'xcal' => $this->importService->importXml(...),
|
||||
'jcal' => $this->importService->importJson(...),
|
||||
default => $this->importService->importText(...)
|
||||
};
|
||||
|
||||
foreach ($vObject->getComponents() as $component) {
|
||||
if ($component->name === 'VTIMEZONE') {
|
||||
continue;
|
||||
foreach ($generator($data) as $vObject) {
|
||||
/** @var Component\VCalendar $vObject */
|
||||
$vBase = $vObject->getBaseComponent();
|
||||
|
||||
if (!$vBase->UID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some calendar providers (e.g. Google, MS) use very long UIDs
|
||||
if (strlen($vBase->UID->getValue()) > 512) {
|
||||
$this->logger->warning('Skipping calendar object with overly long UID from subscription "{subscriptionId}"', [
|
||||
'subscriptionId' => $subscription['id'],
|
||||
'uid' => $vBase->UID->getValue(),
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($stripTodos && $vBase->name === 'VTODO') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($stripAlarms || $stripAttachments) {
|
||||
foreach ($vObject->getComponents() as $component) {
|
||||
if ($component->name === 'VTIMEZONE') {
|
||||
continue;
|
||||
}
|
||||
if ($stripAlarms) {
|
||||
$component->remove('VALARM');
|
||||
}
|
||||
if ($stripAttachments) {
|
||||
$component->remove('ATTACH');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$compName = $component->name;
|
||||
$sObject = $vObject->serialize();
|
||||
$uid = $vBase->UID->getValue();
|
||||
$etag = md5($sObject);
|
||||
|
||||
if ($stripAlarms) {
|
||||
unset($component->{'VALARM'});
|
||||
// No existing object with this UID, create it
|
||||
if (!isset($existingObjects[$uid])) {
|
||||
try {
|
||||
$this->calDavBackend->createCalendarObject(
|
||||
$subscription['id'],
|
||||
UUIDUtil::getUUID() . '.ics',
|
||||
$sObject,
|
||||
CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION
|
||||
);
|
||||
} catch (\Exception $ex) {
|
||||
$this->logger->warning('Unable to create calendar object from subscription {subscriptionId}', [
|
||||
'exception' => $ex,
|
||||
'subscriptionId' => $subscription['id'],
|
||||
'source' => $subscription['source'],
|
||||
]);
|
||||
}
|
||||
if ($stripAttachments) {
|
||||
unset($component->{'ATTACH'});
|
||||
}
|
||||
|
||||
$uid = $component->{ 'UID' }->getValue();
|
||||
}
|
||||
|
||||
if ($stripTodos && $compName === 'VTODO') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($uid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$denormalized = $this->calDavBackend->getDenormalizedData($vObject->serialize());
|
||||
} catch (InvalidDataException|Forbidden $ex) {
|
||||
$this->logger->warning('Unable to denormalize calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find all identical sets and remove them from the update
|
||||
if (isset($localData[$uid]) && $denormalized['etag'] === $localData[$uid]['etag']) {
|
||||
unset($localData[$uid]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$vObjectCopy = clone $vObject;
|
||||
$identical = isset($localData[$uid]) && $this->compareWithoutDtstamp($vObjectCopy, $localData[$uid]);
|
||||
if ($identical) {
|
||||
unset($localData[$uid]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find all modified sets and update them
|
||||
if (isset($localData[$uid]) && $denormalized['etag'] !== $localData[$uid]['etag']) {
|
||||
$this->calDavBackend->updateCalendarObject($subscription['id'], $localData[$uid]['uri'], $vObject->serialize(), CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
|
||||
unset($localData[$uid]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only entirely new events get created here
|
||||
try {
|
||||
$objectUri = $this->getRandomCalendarObjectUri();
|
||||
$this->calDavBackend->createCalendarObject($subscription['id'], $objectUri, $vObject->serialize(), CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
|
||||
} catch (NoInstancesException|BadRequest $ex) {
|
||||
$this->logger->warning('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]);
|
||||
} elseif ($existingObjects[$uid]['etag'] !== $etag) {
|
||||
// Existing object with this UID but different etag, update it
|
||||
$this->calDavBackend->updateCalendarObject(
|
||||
$subscription['id'],
|
||||
$existingObjects[$uid]['uri'],
|
||||
$sObject,
|
||||
CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION
|
||||
);
|
||||
unset($existingObjects[$uid]);
|
||||
} else {
|
||||
// Existing object with same etag, just remove from tracking
|
||||
unset($existingObjects[$uid]);
|
||||
}
|
||||
}
|
||||
|
||||
$ids = array_map(static function ($dataSet): int {
|
||||
return (int)$dataSet['id'];
|
||||
}, $localData);
|
||||
$uris = array_map(static function ($dataSet): string {
|
||||
return $dataSet['uri'];
|
||||
}, $localData);
|
||||
|
||||
if (!empty($ids) && !empty($uris)) {
|
||||
// Clean up on aisle 5
|
||||
// The only events left over in the $localData array should be those that don't exist upstream
|
||||
// All deleted VObjects from upstream are removed
|
||||
$this->calDavBackend->purgeCachedEventsForSubscription($subscription['id'], $ids, $uris);
|
||||
// Clean up objects that no longer exist in the remote feed
|
||||
// The only events left over should be those not found upstream
|
||||
if (!empty($existingObjects)) {
|
||||
$ids = array_map('intval', array_column($existingObjects, 'id'));
|
||||
$uris = array_column($existingObjects, 'uri');
|
||||
$this->calDavBackend->purgeCachedEventsForSubscription((int)$subscription['id'], $ids, $uris);
|
||||
}
|
||||
|
||||
$newRefreshRate = $this->checkWebcalDataForRefreshRate($subscription, $webcalData);
|
||||
if ($newRefreshRate) {
|
||||
$mutations[self::REFRESH_RATE] = $newRefreshRate;
|
||||
// Update refresh rate from the last processed object
|
||||
if (isset($vObject)) {
|
||||
$this->updateRefreshRate($subscription, $vObject);
|
||||
}
|
||||
|
||||
$this->updateSubscription($subscription, $mutations);
|
||||
} catch (ParseException $ex) {
|
||||
$this->logger->error('Subscription {subscriptionId} could not be refreshed due to a parsing error', ['exception' => $ex, 'subscriptionId' => $subscription['id']]);
|
||||
} finally {
|
||||
// Close the data stream to free resources
|
||||
if (is_resource($data)) {
|
||||
fclose($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,84 +184,34 @@ class RefreshWebcalService {
|
|||
return $subscriptions[0];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* check if:
|
||||
* - current subscription stores a refreshrate
|
||||
* - the webcal feed suggests a refreshrate
|
||||
* - return suggested refreshrate if user didn't set a custom one
|
||||
*
|
||||
* Update refresh rate from calendar object if:
|
||||
* - current subscription does not store a refreshrate
|
||||
* - the webcal feed suggests a valid refreshrate
|
||||
*/
|
||||
private function checkWebcalDataForRefreshRate(array $subscription, string $webcalData): ?string {
|
||||
// if there is no refreshrate stored in the database, check the webcal feed
|
||||
// whether it suggests any refresh rate and store that in the database
|
||||
if (isset($subscription[self::REFRESH_RATE]) && $subscription[self::REFRESH_RATE] !== null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var Component\VCalendar $vCalendar */
|
||||
$vCalendar = Reader::read($webcalData);
|
||||
|
||||
$newRefreshRate = null;
|
||||
if (isset($vCalendar->{'X-PUBLISHED-TTL'})) {
|
||||
$newRefreshRate = $vCalendar->{'X-PUBLISHED-TTL'}->getValue();
|
||||
}
|
||||
if (isset($vCalendar->{'REFRESH-INTERVAL'})) {
|
||||
$newRefreshRate = $vCalendar->{'REFRESH-INTERVAL'}->getValue();
|
||||
}
|
||||
|
||||
if (!$newRefreshRate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check if new refresh rate is even valid
|
||||
try {
|
||||
DateTimeParser::parseDuration($newRefreshRate);
|
||||
} catch (InvalidDataException $ex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $newRefreshRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* update subscription stored in database
|
||||
* used to set:
|
||||
* - refreshrate
|
||||
* - source
|
||||
*
|
||||
* @param array $subscription
|
||||
* @param array $mutations
|
||||
*/
|
||||
private function updateSubscription(array $subscription, array $mutations) {
|
||||
if (empty($mutations)) {
|
||||
private function updateRefreshRate(array $subscription, Component\VCalendar $vCalendar): void {
|
||||
// if there is already a refreshrate stored in the database, don't override it
|
||||
if (!empty($subscription[self::REFRESH_RATE])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$propPatch = new PropPatch($mutations);
|
||||
$refreshRate = $vCalendar->{'REFRESH-INTERVAL'}?->getValue()
|
||||
?? $vCalendar->{'X-PUBLISHED-TTL'}?->getValue();
|
||||
|
||||
if ($refreshRate === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if refresh rate is valid
|
||||
try {
|
||||
DateTimeParser::parseDuration($refreshRate);
|
||||
} catch (InvalidDataException) {
|
||||
return;
|
||||
}
|
||||
|
||||
$propPatch = new PropPatch([self::REFRESH_RATE => $refreshRate]);
|
||||
$this->calDavBackend->updateSubscription($subscription['id'], $propPatch);
|
||||
$propPatch->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random uri for a calendar-object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRandomCalendarObjectUri():string {
|
||||
return UUIDUtil::getUUID() . '.ics';
|
||||
}
|
||||
|
||||
private function compareWithoutDtstamp(Component $vObject, array $calendarObject): bool {
|
||||
foreach ($vObject->getComponents() as $component) {
|
||||
unset($component->{'DTSTAMP'});
|
||||
}
|
||||
|
||||
$localVobject = Reader::read($calendarObject['calendardata']);
|
||||
foreach ($localVobject->getComponents() as $component) {
|
||||
unset($component->{'DTSTAMP'});
|
||||
}
|
||||
|
||||
return strcasecmp($localVobject->serialize(), $vObject->serialize()) === 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,12 +89,8 @@ class ConnectionTest extends TestCase {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $result
|
||||
* @param string $contentType
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('urlDataProvider')]
|
||||
public function testConnection(string $url, string $result, string $contentType): void {
|
||||
public function testConnection(string $url, string $contentType, string $expectedFormat): void {
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$subscription = [
|
||||
|
|
@ -123,16 +119,76 @@ class ConnectionTest extends TestCase {
|
|||
->with('https://foo.bar/bla2')
|
||||
->willReturn($response);
|
||||
|
||||
$response->expects($this->once())
|
||||
->method('getBody')
|
||||
->with()
|
||||
->willReturn($result);
|
||||
$response->expects($this->once())
|
||||
->method('getHeader')
|
||||
->with('Content-Type')
|
||||
->willReturn($contentType);
|
||||
|
||||
$this->connection->queryWebcalFeed($subscription);
|
||||
// Create a stream resource to simulate streaming response
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, 'test calendar data');
|
||||
rewind($stream);
|
||||
|
||||
$response->expects($this->once())
|
||||
->method('getBody')
|
||||
->willReturn($stream);
|
||||
|
||||
$output = $this->connection->queryWebcalFeed($subscription);
|
||||
|
||||
$this->assertIsArray($output);
|
||||
$this->assertArrayHasKey('data', $output);
|
||||
$this->assertArrayHasKey('format', $output);
|
||||
$this->assertIsResource($output['data']);
|
||||
$this->assertEquals($expectedFormat, $output['format']);
|
||||
|
||||
// Cleanup
|
||||
if (is_resource($output['data'])) {
|
||||
fclose($output['data']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testConnectionReturnsNullWhenBodyIsNotResource(): void {
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$subscription = [
|
||||
'id' => 42,
|
||||
'uri' => 'sub123',
|
||||
'refreshreate' => 'P1H',
|
||||
'striptodos' => 1,
|
||||
'stripalarms' => 1,
|
||||
'stripattachments' => 1,
|
||||
'source' => 'https://foo.bar/bla2',
|
||||
'lastmodified' => 0,
|
||||
];
|
||||
|
||||
$this->clientService->expects($this->once())
|
||||
->method('newClient')
|
||||
->with()
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getValueString')
|
||||
->with('dav', 'webcalAllowLocalAccess', 'no')
|
||||
->willReturn('no');
|
||||
|
||||
$client->expects($this->once())
|
||||
->method('get')
|
||||
->with('https://foo.bar/bla2')
|
||||
->willReturn($response);
|
||||
|
||||
$response->expects($this->once())
|
||||
->method('getHeader')
|
||||
->with('Content-Type')
|
||||
->willReturn('text/calendar');
|
||||
|
||||
// Return a string instead of a resource
|
||||
$response->expects($this->once())
|
||||
->method('getBody')
|
||||
->willReturn('not a resource');
|
||||
|
||||
$output = $this->connection->queryWebcalFeed($subscription);
|
||||
|
||||
$this->assertNull($output);
|
||||
}
|
||||
|
||||
public static function runLocalURLDataProvider(): array {
|
||||
|
|
@ -156,21 +212,9 @@ class ConnectionTest extends TestCase {
|
|||
|
||||
public static function urlDataProvider(): array {
|
||||
return [
|
||||
[
|
||||
'https://foo.bar/bla2',
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
|
||||
'text/calendar;charset=utf8',
|
||||
],
|
||||
[
|
||||
'https://foo.bar/bla2',
|
||||
'["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]',
|
||||
'application/calendar+json',
|
||||
],
|
||||
[
|
||||
'https://foo.bar/bla2',
|
||||
'<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>',
|
||||
'application/calendar+xml',
|
||||
],
|
||||
['https://foo.bar/bla2', 'text/calendar;charset=utf8', 'ical'],
|
||||
['https://foo.bar/bla2', 'application/calendar+json', 'jcal'],
|
||||
['https://foo.bar/bla2', 'application/calendar+xml', 'xcal'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ declare(strict_types=1);
|
|||
namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Import\ImportService;
|
||||
use OCA\DAV\CalDAV\WebcalCaching\Connection;
|
||||
use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
|
|
@ -23,7 +24,8 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
private CalDavBackend&MockObject $caldavBackend;
|
||||
private Connection&MockObject $connection;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
private ITimeFactory&MockObject $time;
|
||||
private ImportService&MockObject $importService;
|
||||
private ITimeFactory&MockObject $timeFactory;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
@ -31,19 +33,32 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
$this->caldavBackend = $this->createMock(CalDavBackend::class);
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->time = $this->createMock(ITimeFactory::class);
|
||||
$this->importService = $this->createMock(ImportService::class);
|
||||
$this->timeFactory = $this->createMock(ITimeFactory::class);
|
||||
// Default time factory behavior: current time is far in the future so refresh always happens
|
||||
$this->timeFactory->method('getTime')->willReturn(PHP_INT_MAX);
|
||||
$this->timeFactory->method('getDateTime')->willReturn(new \DateTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a resource stream from string content
|
||||
*/
|
||||
private function createStreamFromString(string $content) {
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $content);
|
||||
rewind($stream);
|
||||
return $stream;
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
|
||||
public function testRun(string $body, string $contentType, string $result): void {
|
||||
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
|
||||
->onlyMethods(['getRandomCalendarObjectUri'])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
|
||||
->getMock();
|
||||
|
||||
$refreshWebcalService
|
||||
->method('getRandomCalendarObjectUri')
|
||||
->willReturn('uri-1.ics');
|
||||
public function testRun(string $body, string $format, string $result): void {
|
||||
$refreshWebcalService = new RefreshWebcalService(
|
||||
$this->caldavBackend,
|
||||
$this->logger,
|
||||
$this->connection,
|
||||
$this->timeFactory,
|
||||
$this->importService
|
||||
);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
|
|
@ -71,26 +86,48 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
],
|
||||
]);
|
||||
|
||||
$stream = $this->createStreamFromString($body);
|
||||
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn($result);
|
||||
->willReturn(['data' => $stream, 'format' => $format]);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getLimitedCalendarObjects')
|
||||
->willReturn([]);
|
||||
|
||||
// Create a VCalendar object that will be yielded by the import service
|
||||
$vCalendar = VObject\Reader::read($result);
|
||||
|
||||
$generator = function () use ($vCalendar) {
|
||||
yield $vCalendar;
|
||||
};
|
||||
|
||||
$this->importService->expects(self::once())
|
||||
->method('importText')
|
||||
->willReturn($generator());
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with(42, 'uri-1.ics', $result, 1);
|
||||
->with(
|
||||
'42',
|
||||
self::matchesRegularExpression('/^[a-f0-9-]+\.ics$/'),
|
||||
$result,
|
||||
CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION
|
||||
);
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('identicalDataProvider')]
|
||||
public function testRunIdentical(string $uid, array $calendarObject, string $body, string $contentType, string $result): void {
|
||||
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
|
||||
->onlyMethods(['getRandomCalendarObjectUri'])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
|
||||
->getMock();
|
||||
|
||||
$refreshWebcalService
|
||||
->method('getRandomCalendarObjectUri')
|
||||
->willReturn('uri-1.ics');
|
||||
public function testRunIdentical(string $uid, array $calendarObject, string $body, string $format, string $result): void {
|
||||
$refreshWebcalService = new RefreshWebcalService(
|
||||
$this->caldavBackend,
|
||||
$this->logger,
|
||||
$this->connection,
|
||||
$this->timeFactory,
|
||||
$this->importService
|
||||
);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
|
|
@ -118,78 +155,199 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
],
|
||||
]);
|
||||
|
||||
$stream = $this->createStreamFromString($body);
|
||||
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn(['data' => $stream, 'format' => $format]);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getLimitedCalendarObjects')
|
||||
->willReturn($calendarObject);
|
||||
|
||||
// Create a VCalendar object that will be yielded by the import service
|
||||
$vCalendar = VObject\Reader::read($result);
|
||||
|
||||
$generator = function () use ($vCalendar) {
|
||||
yield $vCalendar;
|
||||
};
|
||||
|
||||
$this->importService->expects(self::once())
|
||||
->method('importText')
|
||||
->willReturn($generator());
|
||||
|
||||
$this->caldavBackend->expects(self::never())
|
||||
->method('createCalendarObject');
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
|
||||
}
|
||||
|
||||
public function testSubscriptionNotFound(): void {
|
||||
$refreshWebcalService = new RefreshWebcalService(
|
||||
$this->caldavBackend,
|
||||
$this->logger,
|
||||
$this->connection,
|
||||
$this->timeFactory,
|
||||
$this->importService
|
||||
);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
->with('principals/users/testuser')
|
||||
->willReturn([]);
|
||||
|
||||
$this->connection->expects(self::never())
|
||||
->method('queryWebcalFeed');
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
|
||||
}
|
||||
|
||||
public function testConnectionReturnsNull(): void {
|
||||
$refreshWebcalService = new RefreshWebcalService(
|
||||
$this->caldavBackend,
|
||||
$this->logger,
|
||||
$this->connection,
|
||||
$this->timeFactory,
|
||||
$this->importService
|
||||
);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
->with('principals/users/testuser')
|
||||
->willReturn([
|
||||
[
|
||||
'id' => '42',
|
||||
'uri' => 'sub123',
|
||||
RefreshWebcalService::STRIP_TODOS => '1',
|
||||
RefreshWebcalService::STRIP_ALARMS => '1',
|
||||
RefreshWebcalService::STRIP_ATTACHMENTS => '1',
|
||||
'source' => 'webcal://foo.bar/bla2',
|
||||
'lastmodified' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn($result);
|
||||
->willReturn(null);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getLimitedCalendarObjects')
|
||||
->willReturn($calendarObject);
|
||||
|
||||
$denormalised = [
|
||||
'etag' => 100,
|
||||
'size' => strlen($calendarObject[$uid]['calendardata']),
|
||||
'uid' => 'sub456'
|
||||
];
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getDenormalizedData')
|
||||
->willReturn($denormalised);
|
||||
$this->importService->expects(self::never())
|
||||
->method('importText');
|
||||
|
||||
$this->caldavBackend->expects(self::never())
|
||||
->method('createCalendarObject');
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub456');
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
|
||||
}
|
||||
|
||||
public function testRunJustUpdated(): void {
|
||||
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
|
||||
->onlyMethods(['getRandomCalendarObjectUri'])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
|
||||
->getMock();
|
||||
|
||||
$refreshWebcalService
|
||||
->method('getRandomCalendarObjectUri')
|
||||
->willReturn('uri-1.ics');
|
||||
public function testDeletedObjectsArePurged(): void {
|
||||
$refreshWebcalService = new RefreshWebcalService(
|
||||
$this->caldavBackend,
|
||||
$this->logger,
|
||||
$this->connection,
|
||||
$this->timeFactory,
|
||||
$this->importService
|
||||
);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
->with('principals/users/testuser')
|
||||
->willReturn([
|
||||
[
|
||||
'id' => '99',
|
||||
'uri' => 'sub456',
|
||||
RefreshWebcalService::REFRESH_RATE => 'P1D',
|
||||
RefreshWebcalService::STRIP_TODOS => '1',
|
||||
RefreshWebcalService::STRIP_ALARMS => '1',
|
||||
RefreshWebcalService::STRIP_ATTACHMENTS => '1',
|
||||
'source' => 'webcal://foo.bar/bla',
|
||||
'lastmodified' => time(),
|
||||
],
|
||||
[
|
||||
'id' => '42',
|
||||
'uri' => 'sub123',
|
||||
RefreshWebcalService::REFRESH_RATE => 'PT1H',
|
||||
RefreshWebcalService::STRIP_TODOS => '1',
|
||||
RefreshWebcalService::STRIP_ALARMS => '1',
|
||||
RefreshWebcalService::STRIP_ATTACHMENTS => '1',
|
||||
'source' => 'webcal://foo.bar/bla2',
|
||||
'lastmodified' => time(),
|
||||
'lastmodified' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$timeMock = $this->createMock(\DateTime::class);
|
||||
$this->time->expects(self::once())
|
||||
->method('getDateTime')
|
||||
->willReturn($timeMock);
|
||||
$timeMock->expects(self::once())
|
||||
->method('getTimestamp')
|
||||
->willReturn(2101724667);
|
||||
$this->time->expects(self::once())
|
||||
->method('getTime')
|
||||
->willReturn(time());
|
||||
$this->connection->expects(self::never())
|
||||
->method('queryWebcalFeed');
|
||||
$body = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Test//Test//EN\r\nBEGIN:VEVENT\r\nUID:new-event\r\nDTSTAMP:20160218T133704Z\r\nDTSTART:20160218T133704Z\r\nSUMMARY:New Event\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
|
||||
$stream = $this->createStreamFromString($body);
|
||||
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn(['data' => $stream, 'format' => 'ical']);
|
||||
|
||||
// Existing objects include one that won't be in the feed
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getLimitedCalendarObjects')
|
||||
->willReturn([
|
||||
'old-deleted-event' => [
|
||||
'id' => 99,
|
||||
'uid' => 'old-deleted-event',
|
||||
'etag' => 'old-etag',
|
||||
'uri' => 'old-event.ics',
|
||||
],
|
||||
]);
|
||||
|
||||
$vCalendar = VObject\Reader::read($body);
|
||||
$generator = function () use ($vCalendar) {
|
||||
yield $vCalendar;
|
||||
};
|
||||
|
||||
$this->importService->expects(self::once())
|
||||
->method('importText')
|
||||
->willReturn($generator());
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('createCalendarObject');
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('purgeCachedEventsForSubscription')
|
||||
->with(42, [99], ['old-event.ics']);
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
|
||||
}
|
||||
|
||||
public function testLongUidIsSkipped(): void {
|
||||
$refreshWebcalService = new RefreshWebcalService(
|
||||
$this->caldavBackend,
|
||||
$this->logger,
|
||||
$this->connection,
|
||||
$this->timeFactory,
|
||||
$this->importService
|
||||
);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
->with('principals/users/testuser')
|
||||
->willReturn([
|
||||
[
|
||||
'id' => '42',
|
||||
'uri' => 'sub123',
|
||||
RefreshWebcalService::STRIP_TODOS => '1',
|
||||
RefreshWebcalService::STRIP_ALARMS => '1',
|
||||
RefreshWebcalService::STRIP_ATTACHMENTS => '1',
|
||||
'source' => 'webcal://foo.bar/bla2',
|
||||
'lastmodified' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
// Create a UID that is longer than 512 characters
|
||||
$longUid = str_repeat('a', 513);
|
||||
$body = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Test//Test//EN\r\nBEGIN:VEVENT\r\nUID:$longUid\r\nDTSTAMP:20160218T133704Z\r\nDTSTART:20160218T133704Z\r\nSUMMARY:Event with long UID\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
|
||||
$stream = $this->createStreamFromString($body);
|
||||
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn(['data' => $stream, 'format' => 'ical']);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getLimitedCalendarObjects')
|
||||
->willReturn([]);
|
||||
|
||||
$vCalendar = VObject\Reader::read($body);
|
||||
$generator = function () use ($vCalendar) {
|
||||
yield $vCalendar;
|
||||
};
|
||||
|
||||
$this->importService->expects(self::once())
|
||||
->method('importText')
|
||||
->willReturn($generator());
|
||||
|
||||
// Event with long UID should be skipped, so createCalendarObject should never be called
|
||||
$this->caldavBackend->expects(self::never())
|
||||
->method('createCalendarObject');
|
||||
|
||||
|
|
@ -197,16 +355,12 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
|
||||
public function testRunCreateCalendarNoException(string $body, string $contentType, string $result): void {
|
||||
public function testRunCreateCalendarNoException(string $body, string $format, string $result): void {
|
||||
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
|
||||
->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription',])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
|
||||
->onlyMethods(['getSubscription'])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->timeFactory, $this->importService])
|
||||
->getMock();
|
||||
|
||||
$refreshWebcalService
|
||||
->method('getRandomCalendarObjectUri')
|
||||
->willReturn('uri-1.ics');
|
||||
|
||||
$refreshWebcalService
|
||||
->method('getSubscription')
|
||||
->willReturn([
|
||||
|
|
@ -220,13 +374,26 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
'lastmodified' => 0,
|
||||
]);
|
||||
|
||||
$stream = $this->createStreamFromString($body);
|
||||
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn($result);
|
||||
->willReturn(['data' => $stream, 'format' => $format]);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with(42, 'uri-1.ics', $result, 1);
|
||||
->method('getLimitedCalendarObjects')
|
||||
->willReturn([]);
|
||||
|
||||
// Create a VCalendar object that will be yielded by the import service
|
||||
$vCalendar = VObject\Reader::read($result);
|
||||
|
||||
$generator = function () use ($vCalendar) {
|
||||
yield $vCalendar;
|
||||
};
|
||||
|
||||
$this->importService->expects(self::once())
|
||||
->method('importText')
|
||||
->willReturn($generator());
|
||||
|
||||
$noInstanceException = new NoInstancesException("can't add calendar object");
|
||||
$this->caldavBackend->expects(self::once())
|
||||
|
|
@ -241,16 +408,12 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')]
|
||||
public function testRunCreateCalendarBadRequest(string $body, string $contentType, string $result): void {
|
||||
public function testRunCreateCalendarBadRequest(string $body, string $format, string $result): void {
|
||||
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
|
||||
->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription'])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
|
||||
->onlyMethods(['getSubscription'])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->timeFactory, $this->importService])
|
||||
->getMock();
|
||||
|
||||
$refreshWebcalService
|
||||
->method('getRandomCalendarObjectUri')
|
||||
->willReturn('uri-1.ics');
|
||||
|
||||
$refreshWebcalService
|
||||
->method('getSubscription')
|
||||
->willReturn([
|
||||
|
|
@ -264,13 +427,26 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
'lastmodified' => 0,
|
||||
]);
|
||||
|
||||
$stream = $this->createStreamFromString($body);
|
||||
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn($result);
|
||||
->willReturn(['data' => $stream, 'format' => $format]);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with(42, 'uri-1.ics', $result, 1);
|
||||
->method('getLimitedCalendarObjects')
|
||||
->willReturn([]);
|
||||
|
||||
// Create a VCalendar object that will be yielded by the import service
|
||||
$vCalendar = VObject\Reader::read($result);
|
||||
|
||||
$generator = function () use ($vCalendar) {
|
||||
yield $vCalendar;
|
||||
};
|
||||
|
||||
$this->importService->expects(self::once())
|
||||
->method('importText')
|
||||
->willReturn($generator());
|
||||
|
||||
$badRequestException = new BadRequest("can't add reach calendar url");
|
||||
$this->caldavBackend->expects(self::once())
|
||||
|
|
@ -285,20 +461,22 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
}
|
||||
|
||||
public static function identicalDataProvider(): array {
|
||||
$icalBody = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
|
||||
$etag = md5($icalBody);
|
||||
|
||||
return [
|
||||
[
|
||||
'12345',
|
||||
[
|
||||
'12345' => [
|
||||
'id' => 42,
|
||||
'etag' => 100,
|
||||
'uri' => 'sub456',
|
||||
'calendardata' => "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
|
||||
'etag' => $etag,
|
||||
'uri' => 'sub456.ics',
|
||||
],
|
||||
],
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
|
||||
'text/calendar;charset=utf8',
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20180218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
|
||||
'ical',
|
||||
$icalBody,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
@ -307,19 +485,9 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
return [
|
||||
[
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
|
||||
'text/calendar;charset=utf8',
|
||||
'ical',
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
|
||||
],
|
||||
[
|
||||
'["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]',
|
||||
'application/calendar+json',
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VTIMEZONE\r\nLAST-MODIFIED:20040110T032845Z\r\nTZID:US/Eastern\r\nBEGIN:DAYLIGHT\r\nDTSTART:20000404T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r\nTZNAME:EDT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nDTSTART:20001026T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=10\r\nTZNAME:EST\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060102T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
|
||||
],
|
||||
[
|
||||
'<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>',
|
||||
'application/calendar+xml',
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060104T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2 bis\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -433,11 +433,6 @@
|
|||
<code><![CDATA[!isset($newProps['filters']['props']) || !is_array($newProps['filters']['props'])]]></code>
|
||||
</TypeDoesNotContainType>
|
||||
</file>
|
||||
<file src="apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php">
|
||||
<InvalidArgument>
|
||||
<code><![CDATA[$webcalData]]></code>
|
||||
</InvalidArgument>
|
||||
</file>
|
||||
<file src="apps/dav/lib/CardDAV/AddressBookImpl.php">
|
||||
<InvalidArgument>
|
||||
<code><![CDATA[$this->getKey()]]></code>
|
||||
|
|
|
|||
Loading…
Reference in a new issue