Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
'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',
Expand Down
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
76 changes: 76 additions & 0 deletions apps/dav/lib/CalDAV/CalDavBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,43 @@ public function restoreCalendar(int $id): void {
}, $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
*
Expand Down Expand Up @@ -3264,6 +3301,45 @@ public function purgeAllCachedEventsForSubscription($subscriptionId) {
}, $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)))

Check notice

Code scanning / Psalm

MissingReturnType

Method OCA\DAV\CalDAV\CalDavBackend::moveCalendar does not have a return type, expecting void
->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();
}

Check notice

Code scanning / Psalm

RiskyTruthyFalsyComparison

Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead.
$this->addChanges($subscriptionId, $calendarObjectUris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
}, $this->db);
}

/**
* Move a calendar from one user to another
*
Expand Down
170 changes: 170 additions & 0 deletions apps/dav/lib/CalDAV/WebcalCaching/Connection.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading