mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 16:50:55 -04:00
feat(webcal): only update modified and deleted events from webcal calendars
Signed-off-by: Anna Larch <anna@nextcloud.com>
This commit is contained in:
parent
cee227ae99
commit
fb94db1cd9
7 changed files with 717 additions and 425 deletions
|
|
@ -117,6 +117,7 @@ return array(
|
|||
'OCA\\DAV\\CalDAV\\Trashbin\\RestoreTarget' => $baseDir . '/../lib/CalDAV/Trashbin/RestoreTarget.php',
|
||||
'OCA\\DAV\\CalDAV\\Trashbin\\TrashbinHome' => $baseDir . '/../lib/CalDAV/Trashbin/TrashbinHome.php',
|
||||
'OCA\\DAV\\CalDAV\\Validation\\CalDavValidatePlugin' => $baseDir . '/../lib/CalDAV/Validation/CalDavValidatePlugin.php',
|
||||
'OCA\\DAV\\CalDAV\\WebcalCaching\\Connection' => $baseDir . '/../lib/CalDAV/WebcalCaching/Connection.php',
|
||||
'OCA\\DAV\\CalDAV\\WebcalCaching\\Plugin' => $baseDir . '/../lib/CalDAV/WebcalCaching/Plugin.php',
|
||||
'OCA\\DAV\\CalDAV\\WebcalCaching\\RefreshWebcalService' => $baseDir . '/../lib/CalDAV/WebcalCaching/RefreshWebcalService.php',
|
||||
'OCA\\DAV\\Capabilities' => $baseDir . '/../lib/Capabilities.php',
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\CalDAV\\Trashbin\\RestoreTarget' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/RestoreTarget.php',
|
||||
'OCA\\DAV\\CalDAV\\Trashbin\\TrashbinHome' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/TrashbinHome.php',
|
||||
'OCA\\DAV\\CalDAV\\Validation\\CalDavValidatePlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Validation/CalDavValidatePlugin.php',
|
||||
'OCA\\DAV\\CalDAV\\WebcalCaching\\Connection' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/Connection.php',
|
||||
'OCA\\DAV\\CalDAV\\WebcalCaching\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/Plugin.php',
|
||||
'OCA\\DAV\\CalDAV\\WebcalCaching\\RefreshWebcalService' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/RefreshWebcalService.php',
|
||||
'OCA\\DAV\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php',
|
||||
|
|
|
|||
|
|
@ -945,6 +945,43 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
}, $this->db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all calendar objects with limited metadata for a calendar
|
||||
*
|
||||
* Every item contains an array with the following keys:
|
||||
* * id - the table row id
|
||||
* * etag - An arbitrary string
|
||||
* * uri - a unique key which will be used to construct the uri. This can
|
||||
* be any arbitrary string.
|
||||
* * calendardata - The iCalendar-compatible calendar data
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param int $calendarType
|
||||
* @return array
|
||||
*/
|
||||
public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->select(['id','uid', 'etag', 'uri', 'calendardata'])
|
||||
->from('calendarobjects')
|
||||
->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
|
||||
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
|
||||
->andWhere($query->expr()->isNull('deleted_at'));
|
||||
$stmt = $query->executeQuery();
|
||||
|
||||
$result = [];
|
||||
while (($row = $stmt->fetch()) !== false) {
|
||||
$result[$row['uid']] = [
|
||||
'id' => $row['id'],
|
||||
'etag' => $row['etag'],
|
||||
'uri' => $row['uri'],
|
||||
'calendardata' => $row['calendardata'],
|
||||
];
|
||||
}
|
||||
$stmt->closeCursor();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all of an user's shares
|
||||
*
|
||||
|
|
@ -3264,6 +3301,45 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
}, $this->db);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $subscriptionId
|
||||
* @param array<int> $calendarObjectIds
|
||||
* @param array<string> $calendarObjectUris
|
||||
*/
|
||||
public function purgeCachedEventsForSubscription(int $subscriptionId, array $calendarObjectIds, array $calendarObjectUris): void {
|
||||
if(empty($calendarObjectUris)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->atomic(function () use ($subscriptionId, $calendarObjectIds, $calendarObjectUris) {
|
||||
foreach (array_chunk($calendarObjectIds, 1000) as $chunk) {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->delete($this->dbObjectPropertiesTable)
|
||||
->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
|
||||
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
|
||||
->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
|
||||
->executeStatement();
|
||||
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->delete('calendarobjects')
|
||||
->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
|
||||
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
|
||||
->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
foreach (array_chunk($calendarObjectUris, 1000) as $chunk) {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->delete('calendarchanges')
|
||||
->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
|
||||
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
|
||||
->andWhere($query->expr()->in('uri', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
|
||||
->executeStatement();
|
||||
}
|
||||
$this->addChanges($subscriptionId, $calendarObjectUris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
|
||||
}, $this->db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a calendar from one user to another
|
||||
*
|
||||
|
|
|
|||
170
apps/dav/lib/CalDAV/WebcalCaching/Connection.php
Normal file
170
apps/dav/lib/CalDAV/WebcalCaching/Connection.php
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\DAV\CalDAV\WebcalCaching;
|
||||
|
||||
use Exception;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Middleware;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\Http\Client\LocalServerException;
|
||||
use OCP\IAppConfig;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Xml\Property\Href;
|
||||
use Sabre\VObject\Reader;
|
||||
|
||||
class Connection {
|
||||
public function __construct(private IClientService $clientService,
|
||||
private IAppConfig $config,
|
||||
private LoggerInterface $logger) {
|
||||
}
|
||||
|
||||
/**
|
||||
* gets webcal feed from remote server
|
||||
*/
|
||||
public function queryWebcalFeed(array $subscription, array &$mutations): ?string {
|
||||
$client = $this->clientService->newClient();
|
||||
|
||||
$didBreak301Chain = false;
|
||||
$latestLocation = null;
|
||||
|
||||
$handlerStack = HandlerStack::create();
|
||||
$handlerStack->push(Middleware::mapRequest(function (RequestInterface $request) {
|
||||
return $request
|
||||
->withHeader('Accept', 'text/calendar, application/calendar+json, application/calendar+xml')
|
||||
->withHeader('User-Agent', 'Nextcloud Webcal Service');
|
||||
}));
|
||||
$handlerStack->push(Middleware::mapResponse(function (ResponseInterface $response) use (&$didBreak301Chain, &$latestLocation) {
|
||||
if (!$didBreak301Chain) {
|
||||
if ($response->getStatusCode() !== 301) {
|
||||
$didBreak301Chain = true;
|
||||
} else {
|
||||
$latestLocation = $response->getHeader('Location');
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}));
|
||||
|
||||
$allowLocalAccess = $this->config->getValueString('dav', 'webcalAllowLocalAccess', 'no');
|
||||
$subscriptionId = $subscription['id'];
|
||||
$url = $this->cleanURL($subscription['source']);
|
||||
if ($url === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$params = [
|
||||
'allow_redirects' => [
|
||||
'redirects' => 10
|
||||
],
|
||||
'handler' => $handlerStack,
|
||||
'nextcloud' => [
|
||||
'allow_local_address' => $allowLocalAccess === 'yes',
|
||||
]
|
||||
];
|
||||
|
||||
$user = parse_url($subscription['source'], PHP_URL_USER);
|
||||
$pass = parse_url($subscription['source'], PHP_URL_PASS);
|
||||
if ($user !== null && $pass !== null) {
|
||||
$params['auth'] = [$user, $pass];
|
||||
}
|
||||
|
||||
$response = $client->get($url, $params);
|
||||
$body = $response->getBody();
|
||||
|
||||
if ($latestLocation !== null) {
|
||||
$mutations['{http://calendarserver.org/ns/}source'] = new Href($latestLocation);
|
||||
}
|
||||
|
||||
$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();
|
||||
|
||||
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();
|
||||
}
|
||||
} catch (LocalServerException $ex) {
|
||||
$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules", [
|
||||
'exception' => $ex,
|
||||
]);
|
||||
|
||||
return null;
|
||||
} catch (Exception $ex) {
|
||||
$this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error", [
|
||||
'exception' => $ex,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will strip authentication information and replace the
|
||||
* 'webcal' or 'webcals' protocol scheme
|
||||
*
|
||||
* @param string $url
|
||||
* @return string|null
|
||||
*/
|
||||
private function cleanURL(string $url): ?string {
|
||||
$parsed = parse_url($url);
|
||||
if ($parsed === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
|
||||
$scheme = 'http';
|
||||
} else {
|
||||
$scheme = 'https';
|
||||
}
|
||||
|
||||
$host = $parsed['host'] ?? '';
|
||||
$port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
|
||||
$path = $parsed['path'] ?? '';
|
||||
$query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
|
||||
$fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
|
||||
|
||||
$cleanURL = "$scheme://$host$port$path$query$fragment";
|
||||
// parse_url is giving some weird results if no url and no :// is given,
|
||||
// so let's test the url again
|
||||
$parsedClean = parse_url($cleanURL);
|
||||
if ($parsedClean === false || !isset($parsedClean['host'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $cleanURL;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,19 +8,11 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\DAV\CalDAV\WebcalCaching;
|
||||
|
||||
use Exception;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Middleware;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\Http\Client\LocalServerException;
|
||||
use OCP\IConfig;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Exception\BadRequest;
|
||||
use Sabre\DAV\PropPatch;
|
||||
use Sabre\DAV\Xml\Property\Href;
|
||||
use Sabre\VObject\Component;
|
||||
use Sabre\VObject\DateTimeParser;
|
||||
use Sabre\VObject\InvalidDataException;
|
||||
|
|
@ -33,25 +25,15 @@ use function count;
|
|||
|
||||
class RefreshWebcalService {
|
||||
|
||||
private CalDavBackend $calDavBackend;
|
||||
|
||||
private IClientService $clientService;
|
||||
|
||||
private IConfig $config;
|
||||
|
||||
/** @var LoggerInterface */
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public const REFRESH_RATE = '{http://apple.com/ns/ical/}refreshrate';
|
||||
public const STRIP_ALARMS = '{http://calendarserver.org/ns/}subscribed-strip-alarms';
|
||||
public const STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments';
|
||||
public const STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos';
|
||||
|
||||
public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, LoggerInterface $logger) {
|
||||
$this->calDavBackend = $calDavBackend;
|
||||
$this->clientService = $clientService;
|
||||
$this->config = $config;
|
||||
$this->logger = $logger;
|
||||
public function __construct(private CalDavBackend $calDavBackend,
|
||||
private LoggerInterface $logger,
|
||||
private Connection $connection,
|
||||
private ITimeFactory $time) {
|
||||
}
|
||||
|
||||
public function refreshSubscription(string $principalUri, string $uri) {
|
||||
|
|
@ -61,11 +43,25 @@ class RefreshWebcalService {
|
|||
return;
|
||||
}
|
||||
|
||||
$webcalData = $this->queryWebcalFeed($subscription, $mutations);
|
||||
// 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']);
|
||||
$updateTime = $this->time->getDateTime();
|
||||
$updateTime->setTimestamp($subscription['lastmodified'])->add($refreshInterval);
|
||||
if($updateTime->getTimestamp() > $this->time->getTime()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$webcalData = $this->connection->queryWebcalFeed($subscription, $mutations);
|
||||
if (!$webcalData) {
|
||||
return;
|
||||
}
|
||||
|
||||
$localData = $this->calDavBackend->getLimitedCalendarObjects((int) $subscription['id'], CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
|
||||
|
||||
$stripTodos = ($subscription[self::STRIP_TODOS] ?? 1) === 1;
|
||||
$stripAlarms = ($subscription[self::STRIP_ALARMS] ?? 1) === 1;
|
||||
$stripAttachments = ($subscription[self::STRIP_ATTACHMENTS] ?? 1) === 1;
|
||||
|
|
@ -73,14 +69,10 @@ class RefreshWebcalService {
|
|||
try {
|
||||
$splitter = new ICalendar($webcalData, Reader::OPTION_FORGIVING);
|
||||
|
||||
// we wait with deleting all outdated events till we parsed the new ones
|
||||
// in case the new calendar is broken and `new ICalendar` throws a ParseException
|
||||
// the user will still see the old data
|
||||
$this->calDavBackend->purgeAllCachedEventsForSubscription($subscription['id']);
|
||||
|
||||
while ($vObject = $splitter->getNext()) {
|
||||
/** @var Component $vObject */
|
||||
$compName = null;
|
||||
$uid = null;
|
||||
|
||||
foreach ($vObject->getComponents() as $component) {
|
||||
if ($component->name === 'VTIMEZONE') {
|
||||
|
|
@ -95,21 +87,62 @@ class RefreshWebcalService {
|
|||
if ($stripAttachments) {
|
||||
unset($component->{'ATTACH'});
|
||||
}
|
||||
|
||||
$uid = $component->{ 'UID' }->getValue();
|
||||
}
|
||||
|
||||
if ($stripTodos && $compName === 'VTODO') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$objectUri = $this->getRandomCalendarObjectUri();
|
||||
$calendarData = $vObject->serialize();
|
||||
if (!isset($uid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$denormalized = $this->calDavBackend->getDenormalizedData($vObject->serialize());
|
||||
// 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 {
|
||||
$this->calDavBackend->createCalendarObject($subscription['id'], $objectUri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
|
||||
$objectUri = $this->getRandomCalendarObjectUri();
|
||||
$this->calDavBackend->createCalendarObject($subscription['id'], $objectUri, $vObject->serialize(), CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
|
||||
} catch (NoInstancesException | BadRequest $ex) {
|
||||
$this->logger->error('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]);
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
$newRefreshRate = $this->checkWebcalDataForRefreshRate($subscription, $webcalData);
|
||||
if ($newRefreshRate) {
|
||||
$mutations[self::REFRESH_RATE] = $newRefreshRate;
|
||||
|
|
@ -139,111 +172,6 @@ class RefreshWebcalService {
|
|||
return $subscriptions[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* gets webcal feed from remote server
|
||||
*/
|
||||
private function queryWebcalFeed(array $subscription, array &$mutations): ?string {
|
||||
$client = $this->clientService->newClient();
|
||||
|
||||
$didBreak301Chain = false;
|
||||
$latestLocation = null;
|
||||
|
||||
$handlerStack = HandlerStack::create();
|
||||
$handlerStack->push(Middleware::mapRequest(function (RequestInterface $request) {
|
||||
return $request
|
||||
->withHeader('Accept', 'text/calendar, application/calendar+json, application/calendar+xml')
|
||||
->withHeader('User-Agent', 'Nextcloud Webcal Service');
|
||||
}));
|
||||
$handlerStack->push(Middleware::mapResponse(function (ResponseInterface $response) use (&$didBreak301Chain, &$latestLocation) {
|
||||
if (!$didBreak301Chain) {
|
||||
if ($response->getStatusCode() !== 301) {
|
||||
$didBreak301Chain = true;
|
||||
} else {
|
||||
$latestLocation = $response->getHeader('Location');
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}));
|
||||
|
||||
$allowLocalAccess = $this->config->getAppValue('dav', 'webcalAllowLocalAccess', 'no');
|
||||
$subscriptionId = $subscription['id'];
|
||||
$url = $this->cleanURL($subscription['source']);
|
||||
if ($url === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$params = [
|
||||
'allow_redirects' => [
|
||||
'redirects' => 10
|
||||
],
|
||||
'handler' => $handlerStack,
|
||||
'nextcloud' => [
|
||||
'allow_local_address' => $allowLocalAccess === 'yes',
|
||||
]
|
||||
];
|
||||
|
||||
$user = parse_url($subscription['source'], PHP_URL_USER);
|
||||
$pass = parse_url($subscription['source'], PHP_URL_PASS);
|
||||
if ($user !== null && $pass !== null) {
|
||||
$params['auth'] = [$user, $pass];
|
||||
}
|
||||
|
||||
$response = $client->get($url, $params);
|
||||
$body = $response->getBody();
|
||||
|
||||
if ($latestLocation) {
|
||||
$mutations['{http://calendarserver.org/ns/}source'] = new Href($latestLocation);
|
||||
}
|
||||
|
||||
$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();
|
||||
|
||||
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();
|
||||
}
|
||||
} catch (LocalServerException $ex) {
|
||||
$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules", [
|
||||
'exception' => $ex,
|
||||
]);
|
||||
|
||||
return null;
|
||||
} catch (Exception $ex) {
|
||||
$this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error", [
|
||||
'exception' => $ex,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check if:
|
||||
|
|
@ -303,42 +231,6 @@ class RefreshWebcalService {
|
|||
$propPatch->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will strip authentication information and replace the
|
||||
* 'webcal' or 'webcals' protocol scheme
|
||||
*
|
||||
* @param string $url
|
||||
* @return string|null
|
||||
*/
|
||||
private function cleanURL(string $url): ?string {
|
||||
$parsed = parse_url($url);
|
||||
if ($parsed === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
|
||||
$scheme = 'http';
|
||||
} else {
|
||||
$scheme = 'https';
|
||||
}
|
||||
|
||||
$host = $parsed['host'] ?? '';
|
||||
$port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
|
||||
$path = $parsed['path'] ?? '';
|
||||
$query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
|
||||
$fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
|
||||
|
||||
$cleanURL = "$scheme://$host$port$path$query$fragment";
|
||||
// parse_url is giving some weird results if no url and no :// is given,
|
||||
// so let's test the url again
|
||||
$parsedClean = parse_url($cleanURL);
|
||||
if ($parsedClean === false || !isset($parsedClean['host'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $cleanURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random uri for a calendar-object
|
||||
*
|
||||
|
|
@ -347,4 +239,17 @@ class RefreshWebcalService {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
192
apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php
Normal file
192
apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching;
|
||||
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use OCA\DAV\CalDAV\WebcalCaching\Connection;
|
||||
use OCP\Http\Client\IClient;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use OCP\Http\Client\LocalServerException;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
use Test\TestCase;
|
||||
|
||||
class ConnectionTest extends TestCase {
|
||||
|
||||
private IClientService|MockObject $clientService;
|
||||
private IConfig|MockObject $config;
|
||||
private LoggerInterface|MockObject $logger;
|
||||
private Connection $connection;
|
||||
|
||||
public function setUp(): void {
|
||||
$this->clientService = $this->createMock(IClientService::class);
|
||||
$this->config = $this->createMock(IAppConfig::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->connection = new Connection($this->clientService, $this->config, $this->logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider runLocalURLDataProvider
|
||||
*/
|
||||
public function testLocalUrl($source) {
|
||||
$subscription = [
|
||||
'id' => 42,
|
||||
'uri' => 'sub123',
|
||||
'refreshreate' => 'P1H',
|
||||
'striptodos' => 1,
|
||||
'stripalarms' => 1,
|
||||
'stripattachments' => 1,
|
||||
'source' => $source,
|
||||
'lastmodified' => 0,
|
||||
];
|
||||
$mutation = [];
|
||||
|
||||
$client = $this->createMock(IClient::class);
|
||||
$this->clientService->expects(self::once())
|
||||
->method('newClient')
|
||||
->with()
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->expects(self::once())
|
||||
->method('getValueString')
|
||||
->with('dav', 'webcalAllowLocalAccess', 'no')
|
||||
->willReturn('no');
|
||||
|
||||
$localServerException = new LocalServerException();
|
||||
$client->expects(self::once())
|
||||
->method('get')
|
||||
->willThrowException($localServerException);
|
||||
$this->logger->expects(self::once())
|
||||
->method('warning')
|
||||
->with("Subscription 42 was not refreshed because it violates local access rules", ['exception' => $localServerException]);
|
||||
|
||||
$this->connection->queryWebcalFeed($subscription, $mutation);
|
||||
}
|
||||
|
||||
public function testInvalidUrl(): void {
|
||||
$subscription = [
|
||||
'id' => 42,
|
||||
'uri' => 'sub123',
|
||||
'refreshreate' => 'P1H',
|
||||
'striptodos' => 1,
|
||||
'stripalarms' => 1,
|
||||
'stripattachments' => 1,
|
||||
'source' => '!@#$',
|
||||
'lastmodified' => 0,
|
||||
];
|
||||
$mutation = [];
|
||||
|
||||
$client = $this->createMock(IClient::class);
|
||||
$this->clientService->expects(self::once())
|
||||
->method('newClient')
|
||||
->with()
|
||||
->willReturn($client);
|
||||
$this->config->expects(self::once())
|
||||
->method('getValueString')
|
||||
->with('dav', 'webcalAllowLocalAccess', 'no')
|
||||
->willReturn('no');
|
||||
|
||||
$client->expects(self::never())
|
||||
->method('get');
|
||||
|
||||
$this->connection->queryWebcalFeed($subscription, $mutation);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $result
|
||||
* @param string $contentType
|
||||
* @dataProvider urlDataProvider
|
||||
*/
|
||||
public function testConnection(string $url, string $result, string $contentType): 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' => $url,
|
||||
'lastmodified' => 0,
|
||||
];
|
||||
$mutation = [];
|
||||
|
||||
$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', $this->callback(function ($obj) {
|
||||
return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
|
||||
}))
|
||||
->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, $mutation);
|
||||
|
||||
}
|
||||
public static function runLocalURLDataProvider(): array {
|
||||
return [
|
||||
['localhost/foo.bar'],
|
||||
['localHost/foo.bar'],
|
||||
['random-host/foo.bar'],
|
||||
['[::1]/bla.blub'],
|
||||
['[::]/bla.blub'],
|
||||
['192.168.0.1'],
|
||||
['172.16.42.1'],
|
||||
['[fdf8:f53b:82e4::53]/secret.ics'],
|
||||
['[fe80::200:5aee:feaa:20a2]/secret.ics'],
|
||||
['[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
|
||||
['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
|
||||
['10.0.0.1'],
|
||||
['another-host.local'],
|
||||
['service.localhost'],
|
||||
];
|
||||
}
|
||||
|
||||
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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching;
|
||||
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\WebcalCaching\Connection;
|
||||
use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
|
||||
use OCP\Http\Client\IClient;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use OCP\Http\Client\LocalServerException;
|
||||
use OCP\IConfig;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Exception\BadRequest;
|
||||
|
|
@ -22,26 +20,18 @@ use Sabre\VObject\Recur\NoInstancesException;
|
|||
use Test\TestCase;
|
||||
|
||||
class RefreshWebcalServiceTest extends TestCase {
|
||||
|
||||
/** @var CalDavBackend | MockObject */
|
||||
private $caldavBackend;
|
||||
|
||||
/** @var IClientService | MockObject */
|
||||
private $clientService;
|
||||
|
||||
/** @var IConfig | MockObject */
|
||||
private $config;
|
||||
|
||||
/** @var LoggerInterface | MockObject */
|
||||
private $logger;
|
||||
private CalDavBackend | MockObject $caldavBackend;
|
||||
private Connection | MockObject $connection;
|
||||
private LoggerInterface | MockObject $logger;
|
||||
private ITimeFactory|MockObject $time;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->caldavBackend = $this->createMock(CalDavBackend::class);
|
||||
$this->clientService = $this->createMock(IClientService::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->time = $this->createMock(ITimeFactory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -54,76 +44,170 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
public function testRun(string $body, string $contentType, string $result): void {
|
||||
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
|
||||
->onlyMethods(['getRandomCalendarObjectUri'])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->clientService, $this->config, $this->logger])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
|
||||
->getMock();
|
||||
|
||||
$refreshWebcalService
|
||||
->method('getRandomCalendarObjectUri')
|
||||
->willReturn('uri-1.ics');
|
||||
|
||||
$this->caldavBackend->expects($this->once())
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getSubscriptionsForUser')
|
||||
->with('principals/users/testuser')
|
||||
->willReturn([
|
||||
[
|
||||
'id' => '99',
|
||||
'uri' => 'sub456',
|
||||
'{http://apple.com/ns/ical/}refreshrate' => 'P1D',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
|
||||
'source' => 'webcal://foo.bar/bla'
|
||||
RefreshWebcalService::REFRESH_RATE => 'P1D',
|
||||
RefreshWebcalService::STRIP_TODOS => '1',
|
||||
RefreshWebcalService::STRIP_ALARMS => '1',
|
||||
RefreshWebcalService::STRIP_ATTACHMENTS => '1',
|
||||
'source' => 'webcal://foo.bar/bla',
|
||||
'lastmodified' => 0,
|
||||
],
|
||||
[
|
||||
'id' => '42',
|
||||
'uri' => 'sub123',
|
||||
'{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
|
||||
'source' => 'webcal://foo.bar/bla2'
|
||||
RefreshWebcalService::REFRESH_RATE => 'PT1H',
|
||||
RefreshWebcalService::STRIP_TODOS => '1',
|
||||
RefreshWebcalService::STRIP_ALARMS => '1',
|
||||
RefreshWebcalService::STRIP_ATTACHMENTS => '1',
|
||||
'source' => 'webcal://foo.bar/bla2',
|
||||
'lastmodified' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$this->clientService->expects($this->once())
|
||||
->method('newClient')
|
||||
->with()
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getAppValue')
|
||||
->with('dav', 'webcalAllowLocalAccess', 'no')
|
||||
->willReturn('no');
|
||||
|
||||
$client->expects($this->once())
|
||||
->method('get')
|
||||
->with('https://foo.bar/bla2', $this->callback(function ($obj) {
|
||||
return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
|
||||
}))
|
||||
->willReturn($response);
|
||||
|
||||
$response->expects($this->once())
|
||||
->method('getBody')
|
||||
->with()
|
||||
->willReturn($body);
|
||||
$response->expects($this->once())
|
||||
->method('getHeader')
|
||||
->with('Content-Type')
|
||||
->willReturn($contentType);
|
||||
|
||||
$this->caldavBackend->expects($this->once())
|
||||
->method('purgeAllCachedEventsForSubscription')
|
||||
->with(42);
|
||||
|
||||
$this->caldavBackend->expects($this->once())
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn($result);
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with(42, 'uri-1.ics', $result, 1);
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $body
|
||||
* @param string $contentType
|
||||
* @param string $result
|
||||
*
|
||||
* @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');
|
||||
|
||||
$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' => 0,
|
||||
],
|
||||
[
|
||||
'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' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn($result);
|
||||
|
||||
$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->caldavBackend->expects(self::never())
|
||||
->method('createCalendarObject');
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub456');
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
$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(),
|
||||
],
|
||||
]);
|
||||
|
||||
$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');
|
||||
$this->caldavBackend->expects(self::never())
|
||||
->method('createCalendarObject');
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $body
|
||||
* @param string $contentType
|
||||
|
|
@ -132,11 +216,9 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
* @dataProvider runDataProvider
|
||||
*/
|
||||
public function testRunCreateCalendarNoException(string $body, string $contentType, string $result): void {
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
|
||||
->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription', 'queryWebcalFeed'])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->clientService, $this->config, $this->logger])
|
||||
->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription',])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
|
||||
->getMock();
|
||||
|
||||
$refreshWebcalService
|
||||
|
|
@ -148,53 +230,28 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
->willReturn([
|
||||
'id' => '42',
|
||||
'uri' => 'sub123',
|
||||
'{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
|
||||
'source' => 'webcal://foo.bar/bla2'
|
||||
RefreshWebcalService::REFRESH_RATE => 'PT1H',
|
||||
RefreshWebcalService::STRIP_TODOS => '1',
|
||||
RefreshWebcalService::STRIP_ALARMS => '1',
|
||||
RefreshWebcalService::STRIP_ATTACHMENTS => '1',
|
||||
'source' => 'webcal://foo.bar/bla2',
|
||||
'lastmodified' => 0,
|
||||
]);
|
||||
|
||||
$this->clientService->expects($this->once())
|
||||
->method('newClient')
|
||||
->with()
|
||||
->willReturn($client);
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn($result);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getAppValue')
|
||||
->with('dav', 'webcalAllowLocalAccess', 'no')
|
||||
->willReturn('no');
|
||||
|
||||
$client->expects($this->once())
|
||||
->method('get')
|
||||
->with('https://foo.bar/bla2', $this->callback(function ($obj) {
|
||||
return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
|
||||
}))
|
||||
->willReturn($response);
|
||||
|
||||
$response->expects($this->once())
|
||||
->method('getBody')
|
||||
->with()
|
||||
->willReturn($body);
|
||||
$response->expects($this->once())
|
||||
->method('getHeader')
|
||||
->with('Content-Type')
|
||||
->willReturn($contentType);
|
||||
|
||||
$this->caldavBackend->expects($this->once())
|
||||
->method('purgeAllCachedEventsForSubscription')
|
||||
->with(42);
|
||||
|
||||
$this->caldavBackend->expects($this->once())
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with(42, 'uri-1.ics', $result, 1);
|
||||
|
||||
$noInstanceException = new NoInstancesException("can't add calendar object");
|
||||
$this->caldavBackend->expects($this->once())
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method("createCalendarObject")
|
||||
->willThrowException($noInstanceException);
|
||||
|
||||
$this->logger->expects($this->once())
|
||||
$this->logger->expects(self::once())
|
||||
->method('error')
|
||||
->with('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $noInstanceException, 'subscriptionId' => '42', 'source' => 'webcal://foo.bar/bla2']);
|
||||
|
||||
|
|
@ -209,11 +266,9 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
* @dataProvider runDataProvider
|
||||
*/
|
||||
public function testRunCreateCalendarBadRequest(string $body, string $contentType, string $result): void {
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class)
|
||||
->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription', 'queryWebcalFeed'])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->clientService, $this->config, $this->logger])
|
||||
->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription'])
|
||||
->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time])
|
||||
->getMock();
|
||||
|
||||
$refreshWebcalService
|
||||
|
|
@ -225,59 +280,56 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
->willReturn([
|
||||
'id' => '42',
|
||||
'uri' => 'sub123',
|
||||
'{http://apple.com/ns/ical/}refreshrate' => 'PT1H',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1',
|
||||
'source' => 'webcal://foo.bar/bla2'
|
||||
RefreshWebcalService::REFRESH_RATE => 'PT1H',
|
||||
RefreshWebcalService::STRIP_TODOS => '1',
|
||||
RefreshWebcalService::STRIP_ALARMS => '1',
|
||||
RefreshWebcalService::STRIP_ATTACHMENTS => '1',
|
||||
'source' => 'webcal://foo.bar/bla2',
|
||||
'lastmodified' => 0,
|
||||
]);
|
||||
|
||||
$this->clientService->expects($this->once())
|
||||
->method('newClient')
|
||||
->with()
|
||||
->willReturn($client);
|
||||
$this->connection->expects(self::once())
|
||||
->method('queryWebcalFeed')
|
||||
->willReturn($result);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getAppValue')
|
||||
->with('dav', 'webcalAllowLocalAccess', 'no')
|
||||
->willReturn('no');
|
||||
|
||||
$client->expects($this->once())
|
||||
->method('get')
|
||||
->with('https://foo.bar/bla2', $this->callback(function ($obj) {
|
||||
return $obj['allow_redirects']['redirects'] === 10 && $obj['handler'] instanceof HandlerStack;
|
||||
}))
|
||||
->willReturn($response);
|
||||
|
||||
$response->expects($this->once())
|
||||
->method('getBody')
|
||||
->with()
|
||||
->willReturn($body);
|
||||
$response->expects($this->once())
|
||||
->method('getHeader')
|
||||
->with('Content-Type')
|
||||
->willReturn($contentType);
|
||||
|
||||
$this->caldavBackend->expects($this->once())
|
||||
->method('purgeAllCachedEventsForSubscription')
|
||||
->with(42);
|
||||
|
||||
$this->caldavBackend->expects($this->once())
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with(42, 'uri-1.ics', $result, 1);
|
||||
|
||||
$badRequestException = new BadRequest("can't add reach calendar url");
|
||||
$this->caldavBackend->expects($this->once())
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method("createCalendarObject")
|
||||
->willThrowException($badRequestException);
|
||||
|
||||
$this->logger->expects($this->once())
|
||||
$this->logger->expects(self::once())
|
||||
->method('error')
|
||||
->with('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $badRequestException, 'subscriptionId' => '42', 'source' => 'webcal://foo.bar/bla2']);
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function identicalDataProvider():array {
|
||||
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",
|
||||
],
|
||||
],
|
||||
"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",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
|
|
@ -300,109 +352,4 @@ class RefreshWebcalServiceTest extends TestCase {
|
|||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider runLocalURLDataProvider
|
||||
*/
|
||||
public function testRunLocalURL(string $source): void {
|
||||
$refreshWebcalService = new RefreshWebcalService(
|
||||
$this->caldavBackend,
|
||||
$this->clientService,
|
||||
$this->config,
|
||||
$this->logger
|
||||
);
|
||||
|
||||
$this->caldavBackend->expects($this->once())
|
||||
->method('getSubscriptionsForUser')
|
||||
->with('principals/users/testuser')
|
||||
->willReturn([
|
||||
[
|
||||
'id' => 42,
|
||||
'uri' => 'sub123',
|
||||
'refreshreate' => 'P1H',
|
||||
'striptodos' => 1,
|
||||
'stripalarms' => 1,
|
||||
'stripattachments' => 1,
|
||||
'source' => $source
|
||||
],
|
||||
]);
|
||||
|
||||
$client = $this->createMock(IClient::class);
|
||||
$this->clientService->expects($this->once())
|
||||
->method('newClient')
|
||||
->with()
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getAppValue')
|
||||
->with('dav', 'webcalAllowLocalAccess', 'no')
|
||||
->willReturn('no');
|
||||
|
||||
$localServerException = new LocalServerException();
|
||||
|
||||
$client->expects($this->once())
|
||||
->method('get')
|
||||
->willThrowException($localServerException);
|
||||
|
||||
$this->logger->expects($this->once())
|
||||
->method('warning')
|
||||
->with("Subscription 42 was not refreshed because it violates local access rules", ['exception' => $localServerException]);
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
|
||||
}
|
||||
|
||||
public function runLocalURLDataProvider():array {
|
||||
return [
|
||||
['localhost/foo.bar'],
|
||||
['localHost/foo.bar'],
|
||||
['random-host/foo.bar'],
|
||||
['[::1]/bla.blub'],
|
||||
['[::]/bla.blub'],
|
||||
['192.168.0.1'],
|
||||
['172.16.42.1'],
|
||||
['[fdf8:f53b:82e4::53]/secret.ics'],
|
||||
['[fe80::200:5aee:feaa:20a2]/secret.ics'],
|
||||
['[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
|
||||
['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
|
||||
['10.0.0.1'],
|
||||
['another-host.local'],
|
||||
['service.localhost'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testInvalidUrl(): void {
|
||||
$refreshWebcalService = new RefreshWebcalService($this->caldavBackend,
|
||||
$this->clientService, $this->config, $this->logger);
|
||||
|
||||
$this->caldavBackend->expects($this->once())
|
||||
->method('getSubscriptionsForUser')
|
||||
->with('principals/users/testuser')
|
||||
->willReturn([
|
||||
[
|
||||
'id' => 42,
|
||||
'uri' => 'sub123',
|
||||
'refreshreate' => 'P1H',
|
||||
'striptodos' => 1,
|
||||
'stripalarms' => 1,
|
||||
'stripattachments' => 1,
|
||||
'source' => '!@#$'
|
||||
],
|
||||
]);
|
||||
|
||||
$client = $this->createMock(IClient::class);
|
||||
$this->clientService->expects($this->once())
|
||||
->method('newClient')
|
||||
->with()
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getAppValue')
|
||||
->with('dav', 'webcalAllowLocalAccess', 'no')
|
||||
->willReturn('no');
|
||||
|
||||
$client->expects($this->never())
|
||||
->method('get');
|
||||
|
||||
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue