Skip to content

Commit 75edec9

Browse files
Merge pull request #56784 from nextcloud/fix/calendar-subscription-memory-exhaustion
fix: calendar subscription memory exhaustion
2 parents 4185dfb + 1a0535a commit 75edec9

File tree

7 files changed

+476
-329
lines changed

7 files changed

+476
-329
lines changed

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,9 +1066,9 @@ public function exportCalendar(int $calendarId, int $calendarType = self::CALEND
10661066
* @param int $calendarType
10671067
* @return array
10681068
*/
1069-
public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1069+
public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR, array $fields = []):array {
10701070
$query = $this->db->getQueryBuilder();
1071-
$query->select(['id','uid', 'etag', 'uri', 'calendardata'])
1071+
$query->select($fields ?: ['id', 'uid', 'etag', 'uri', 'calendardata'])
10721072
->from('calendarobjects')
10731073
->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
10741074
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
@@ -1077,12 +1077,7 @@ public function getLimitedCalendarObjects(int $calendarId, int $calendarType = s
10771077

10781078
$result = [];
10791079
while (($row = $stmt->fetchAssociative()) !== false) {
1080-
$result[$row['uid']] = [
1081-
'id' => $row['id'],
1082-
'etag' => $row['etag'],
1083-
'uri' => $row['uri'],
1084-
'calendardata' => $row['calendardata'],
1085-
];
1080+
$result[$row['uid']] = $row;
10861081
}
10871082
$stmt->closeCursor();
10881083

apps/dav/lib/CalDAV/Import/ImportService.php

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@
2323
*/
2424
class ImportService {
2525

26-
/** @var resource */
27-
private $source;
28-
2926
public function __construct(
3027
private CalDavBackend $backend,
3128
) {
@@ -44,18 +41,15 @@ public function import($source, CalendarImpl $calendar, CalendarImportOptions $o
4441
if (!is_resource($source)) {
4542
throw new InvalidArgumentException('Invalid import source must be a file resource');
4643
}
47-
48-
$this->source = $source;
49-
5044
switch ($options->getFormat()) {
5145
case 'ical':
52-
return $this->importProcess($calendar, $options, $this->importText(...));
46+
return $this->importProcess($source, $calendar, $options, $this->importText(...));
5347
break;
5448
case 'jcal':
55-
return $this->importProcess($calendar, $options, $this->importJson(...));
49+
return $this->importProcess($source, $calendar, $options, $this->importJson(...));
5650
break;
5751
case 'xcal':
58-
return $this->importProcess($calendar, $options, $this->importXml(...));
52+
return $this->importProcess($source, $calendar, $options, $this->importXml(...));
5953
break;
6054
default:
6155
throw new InvalidArgumentException('Invalid import format');
@@ -65,10 +59,15 @@ public function import($source, CalendarImpl $calendar, CalendarImportOptions $o
6559
/**
6660
* Generates object stream from a text formatted source (ical)
6761
*
62+
* @param resource $source
63+
*
6864
* @return Generator<\Sabre\VObject\Component\VCalendar>
6965
*/
70-
private function importText(): Generator {
71-
$importer = new TextImporter($this->source);
66+
public function importText($source): Generator {
67+
if (!is_resource($source)) {
68+
throw new InvalidArgumentException('Invalid import source must be a file resource');
69+
}
70+
$importer = new TextImporter($source);
7271
$structure = $importer->structure();
7372
$sObjectPrefix = $importer::OBJECT_PREFIX;
7473
$sObjectSuffix = $importer::OBJECT_SUFFIX;
@@ -113,10 +112,15 @@ private function importText(): Generator {
113112
/**
114113
* Generates object stream from a xml formatted source (xcal)
115114
*
115+
* @param resource $source
116+
*
116117
* @return Generator<\Sabre\VObject\Component\VCalendar>
117118
*/
118-
private function importXml(): Generator {
119-
$importer = new XmlImporter($this->source);
119+
public function importXml($source): Generator {
120+
if (!is_resource($source)) {
121+
throw new InvalidArgumentException('Invalid import source must be a file resource');
122+
}
123+
$importer = new XmlImporter($source);
120124
$structure = $importer->structure();
121125
$sObjectPrefix = $importer::OBJECT_PREFIX;
122126
$sObjectSuffix = $importer::OBJECT_SUFFIX;
@@ -155,11 +159,16 @@ private function importXml(): Generator {
155159
/**
156160
* Generates object stream from a json formatted source (jcal)
157161
*
162+
* @param resource $source
163+
*
158164
* @return Generator<\Sabre\VObject\Component\VCalendar>
159165
*/
160-
private function importJson(): Generator {
166+
public function importJson($source): Generator {
167+
if (!is_resource($source)) {
168+
throw new InvalidArgumentException('Invalid import source must be a file resource');
169+
}
161170
/** @var VCALENDAR $importer */
162-
$importer = Reader::readJson($this->source);
171+
$importer = Reader::readJson($source);
163172
// calendar time zones
164173
$timezones = [];
165174
foreach ($importer->VTIMEZONE as $timezone) {
@@ -212,17 +221,18 @@ private function findTimeZones(VCalendar $vObject): array {
212221
*
213222
* @since 32.0.0
214223
*
224+
* @param resource $source
215225
* @param CalendarImportOptions $options
216226
* @param callable $generator<CalendarImportOptions>: Generator<\Sabre\VObject\Component\VCalendar>
217227
*
218228
* @return array<string,array<string,string|array<string>>>
219229
*/
220-
public function importProcess(CalendarImpl $calendar, CalendarImportOptions $options, callable $generator): array {
230+
public function importProcess($source, CalendarImpl $calendar, CalendarImportOptions $options, callable $generator): array {
221231
$calendarId = $calendar->getKey();
222232
$calendarUri = $calendar->getUri();
223233
$principalUri = $calendar->getPrincipalUri();
224234
$outcome = [];
225-
foreach ($generator() as $vObject) {
235+
foreach ($generator($source) as $vObject) {
226236
$components = $vObject->getBaseComponents();
227237
// determine if the object has no base component types
228238
if (count($components) === 0) {

apps/dav/lib/CalDAV/WebcalCaching/Connection.php

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
use OCP\Http\Client\LocalServerException;
1515
use OCP\IAppConfig;
1616
use Psr\Log\LoggerInterface;
17-
use Sabre\VObject\Reader;
1817

1918
class Connection {
2019
public function __construct(
@@ -26,8 +25,10 @@ public function __construct(
2625

2726
/**
2827
* gets webcal feed from remote server
28+
*
29+
* @return array{data: resource, format: string}|null
2930
*/
30-
public function queryWebcalFeed(array $subscription): ?string {
31+
public function queryWebcalFeed(array $subscription): ?array {
3132
$subscriptionId = $subscription['id'];
3233
$url = $this->cleanURL($subscription['source']);
3334
if ($url === null) {
@@ -54,6 +55,7 @@ public function queryWebcalFeed(array $subscription): ?string {
5455
'User-Agent' => $uaString,
5556
'Accept' => 'text/calendar, application/calendar+json, application/calendar+xml',
5657
],
58+
'stream' => true,
5759
];
5860

5961
$user = parse_url($subscription['source'], PHP_URL_USER);
@@ -77,42 +79,22 @@ public function queryWebcalFeed(array $subscription): ?string {
7779
return null;
7880
}
7981

80-
$body = $response->getBody();
81-
8282
$contentType = $response->getHeader('Content-Type');
8383
$contentType = explode(';', $contentType, 2)[0];
84-
switch ($contentType) {
85-
case 'application/calendar+json':
86-
try {
87-
$jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
88-
} catch (Exception $ex) {
89-
// In case of a parsing error return null
90-
$this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
91-
return null;
92-
}
93-
return $jCalendar->serialize();
94-
95-
case 'application/calendar+xml':
96-
try {
97-
$xCalendar = Reader::readXML($body);
98-
} catch (Exception $ex) {
99-
// In case of a parsing error return null
100-
$this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
101-
return null;
102-
}
103-
return $xCalendar->serialize();
104-
105-
case 'text/calendar':
106-
default:
107-
try {
108-
$vCalendar = Reader::read($body);
109-
} catch (Exception $ex) {
110-
// In case of a parsing error return null
111-
$this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
112-
return null;
113-
}
114-
return $vCalendar->serialize();
84+
85+
$format = match ($contentType) {
86+
'application/calendar+json' => 'jcal',
87+
'application/calendar+xml' => 'xcal',
88+
default => 'ical',
89+
};
90+
91+
// With 'stream' => true, getBody() returns the underlying stream resource
92+
$stream = $response->getBody();
93+
if (!is_resource($stream)) {
94+
return null;
11595
}
96+
97+
return ['data' => $stream, 'format' => $format];
11698
}
11799

118100
/**

0 commit comments

Comments
 (0)