feat(webcal): only update modified and deleted events from webcal calendars

Signed-off-by: Anna Larch <anna@nextcloud.com>
pull/46723/head
Anna Larch 2 years ago
parent cee227ae99
commit fb94db1cd9
  1. 1
      apps/dav/composer/composer/autoload_classmap.php
  2. 1
      apps/dav/composer/composer/autoload_static.php
  3. 76
      apps/dav/lib/CalDAV/CalDavBackend.php
  4. 170
      apps/dav/lib/CalDAV/WebcalCaching/Connection.php
  5. 249
      apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
  6. 192
      apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php
  7. 455
      apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php

@ -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
*

@ -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:
@ -304,47 +232,24 @@ class RefreshWebcalService {
}
/**
* This method will strip authentication information and replace the
* 'webcal' or 'webcals' protocol scheme
* Returns a random uri for a calendar-object
*
* @param string $url
* @return string|null
* @return string
*/
private function cleanURL(string $url): ?string {
$parsed = parse_url($url);
if ($parsed === false) {
return null;
}
public function getRandomCalendarObjectUri():string {
return UUIDUtil::getUUID() . '.ics';
}
if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
$scheme = 'http';
} else {
$scheme = 'https';
private function compareWithoutDtstamp(Component $vObject, array $calendarObject): bool {
foreach ($vObject->getComponents() as $component) {
unset($component->{'DTSTAMP'});
}
$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;
$localVobject = Reader::read($calendarObject['calendardata']);
foreach ($localVobject->getComponents() as $component) {
unset($component->{'DTSTAMP'});
}
return $cleanURL;
}
/**
* Returns a random uri for a calendar-object
*
* @return string
*/
public function getRandomCalendarObjectUri():string {
return UUIDUtil::getUUID() . '.ics';
return strcasecmp($localVobject->serialize(), $vObject->serialize()) === 0;
}
}

@ -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->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);
$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->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);
$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…
Cancel
Save