Passed
Push — master ( b93e1e...342765 )
by Roeland
34:24 queued 23:05
created
apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php 2 patches
Indentation   +369 added lines, -369 removed lines patch added patch discarded remove patch
@@ -52,373 +52,373 @@
 block discarded – undo
52 52
 
53 53
 class RefreshWebcalService {
54 54
 
55
-	/** @var CalDavBackend */
56
-	private $calDavBackend;
57
-
58
-	/** @var IClientService */
59
-	private $clientService;
60
-
61
-	/** @var IConfig */
62
-	private $config;
63
-
64
-	/** @var ILogger */
65
-	private $logger;
66
-
67
-	public const REFRESH_RATE = '{http://apple.com/ns/ical/}refreshrate';
68
-	public const STRIP_ALARMS = '{http://calendarserver.org/ns/}subscribed-strip-alarms';
69
-	public const STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments';
70
-	public const STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos';
71
-
72
-	/**
73
-	 * RefreshWebcalJob constructor.
74
-	 *
75
-	 * @param CalDavBackend $calDavBackend
76
-	 * @param IClientService $clientService
77
-	 * @param IConfig $config
78
-	 * @param ILogger $logger
79
-	 */
80
-	public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, ILogger $logger) {
81
-		$this->calDavBackend = $calDavBackend;
82
-		$this->clientService = $clientService;
83
-		$this->config = $config;
84
-		$this->logger = $logger;
85
-	}
86
-
87
-	/**
88
-	 * @param string $principalUri
89
-	 * @param string $uri
90
-	 */
91
-	public function refreshSubscription(string $principalUri, string $uri) {
92
-		$subscription = $this->getSubscription($principalUri, $uri);
93
-		$mutations = [];
94
-		if (!$subscription) {
95
-			return;
96
-		}
97
-
98
-		$webcalData = $this->queryWebcalFeed($subscription, $mutations);
99
-		if (!$webcalData) {
100
-			return;
101
-		}
102
-
103
-		$stripTodos = ($subscription[self::STRIP_TODOS] ?? 1) === 1;
104
-		$stripAlarms = ($subscription[self::STRIP_ALARMS] ?? 1) === 1;
105
-		$stripAttachments = ($subscription[self::STRIP_ATTACHMENTS] ?? 1) === 1;
106
-
107
-		try {
108
-			$splitter = new ICalendar($webcalData, Reader::OPTION_FORGIVING);
109
-
110
-			// we wait with deleting all outdated events till we parsed the new ones
111
-			// in case the new calendar is broken and `new ICalendar` throws a ParseException
112
-			// the user will still see the old data
113
-			$this->calDavBackend->purgeAllCachedEventsForSubscription($subscription['id']);
114
-
115
-			while ($vObject = $splitter->getNext()) {
116
-				/** @var Component $vObject */
117
-				$compName = null;
118
-
119
-				foreach ($vObject->getComponents() as $component) {
120
-					if ($component->name === 'VTIMEZONE') {
121
-						continue;
122
-					}
123
-
124
-					$compName = $component->name;
125
-
126
-					if ($stripAlarms) {
127
-						unset($component->{'VALARM'});
128
-					}
129
-					if ($stripAttachments) {
130
-						unset($component->{'ATTACH'});
131
-					}
132
-				}
133
-
134
-				if ($stripTodos && $compName === 'VTODO') {
135
-					continue;
136
-				}
137
-
138
-				$uri = $this->getRandomCalendarObjectUri();
139
-				$calendarData = $vObject->serialize();
140
-				try {
141
-					$this->calDavBackend->createCalendarObject($subscription['id'], $uri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
142
-				} catch(BadRequest $ex) {
143
-					$this->logger->logException($ex);
144
-				}
145
-			}
146
-
147
-			$newRefreshRate = $this->checkWebcalDataForRefreshRate($subscription, $webcalData);
148
-			if ($newRefreshRate) {
149
-				$mutations[self::REFRESH_RATE] = $newRefreshRate;
150
-			}
151
-
152
-			$this->updateSubscription($subscription, $mutations);
153
-		} catch(ParseException $ex) {
154
-			$subscriptionId = $subscription['id'];
155
-
156
-			$this->logger->logException($ex);
157
-			$this->logger->warning("Subscription $subscriptionId could not be refreshed due to a parsing error");
158
-		}
159
-	}
160
-
161
-	/**
162
-	 * loads subscription from backend
163
-	 *
164
-	 * @param string $principalUri
165
-	 * @param string $uri
166
-	 * @return array|null
167
-	 */
168
-	public function getSubscription(string $principalUri, string $uri) {
169
-		$subscriptions = array_values(array_filter(
170
-			$this->calDavBackend->getSubscriptionsForUser($principalUri),
171
-			function($sub) use ($uri) {
172
-				return $sub['uri'] === $uri;
173
-			}
174
-		));
175
-
176
-		if (count($subscriptions) === 0) {
177
-			return null;
178
-		}
179
-
180
-		return $subscriptions[0];
181
-	}
182
-
183
-	/**
184
-	 * gets webcal feed from remote server
185
-	 *
186
-	 * @param array $subscription
187
-	 * @param array &$mutations
188
-	 * @return null|string
189
-	 */
190
-	private function queryWebcalFeed(array $subscription, array &$mutations) {
191
-		$client = $this->clientService->newClient();
192
-
193
-		$didBreak301Chain = false;
194
-		$latestLocation = null;
195
-
196
-		$handlerStack = HandlerStack::create();
197
-		$handlerStack->push(Middleware::mapRequest(function (RequestInterface $request) {
198
-			return $request
199
-				->withHeader('Accept', 'text/calendar, application/calendar+json, application/calendar+xml')
200
-				->withHeader('User-Agent', 'Nextcloud Webcal Crawler');
201
-		}));
202
-		$handlerStack->push(Middleware::mapResponse(function(ResponseInterface $response) use (&$didBreak301Chain, &$latestLocation) {
203
-			if (!$didBreak301Chain) {
204
-				if ($response->getStatusCode() !== 301) {
205
-					$didBreak301Chain = true;
206
-				} else {
207
-					$latestLocation = $response->getHeader('Location');
208
-				}
209
-			}
210
-			return $response;
211
-		}));
212
-
213
-		$allowLocalAccess = $this->config->getAppValue('dav', 'webcalAllowLocalAccess', 'no');
214
-		$subscriptionId = $subscription['id'];
215
-		$url = $this->cleanURL($subscription['source']);
216
-		if ($url === null) {
217
-			return null;
218
-		}
219
-
220
-		if ($allowLocalAccess !== 'yes') {
221
-			$host = strtolower(parse_url($url, PHP_URL_HOST));
222
-			// remove brackets from IPv6 addresses
223
-			if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
224
-				$host = substr($host, 1, -1);
225
-			}
226
-
227
-			// Disallow localhost and local network
228
-			if ($host === 'localhost' || substr($host, -6) === '.local' || substr($host, -10) === '.localhost') {
229
-				$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
230
-				return null;
231
-			}
232
-
233
-			// Disallow hostname only
234
-			if (substr_count($host, '.') === 0) {
235
-				$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
236
-				return null;
237
-			}
238
-
239
-			if ((bool)filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
240
-				$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
241
-				return null;
242
-			}
243
-
244
-			// Also check for IPv6 IPv4 nesting, because that's not covered by filter_var
245
-			if ((bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
246
-				$delimiter = strrpos($host, ':'); // Get last colon
247
-				$ipv4Address = substr($host, $delimiter + 1);
248
-
249
-				if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
250
-					$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
251
-					return null;
252
-				}
253
-			}
254
-		}
255
-
256
-		try {
257
-			$params = [
258
-				'allow_redirects' => [
259
-					'redirects' => 10
260
-				],
261
-				'handler' => $handlerStack,
262
-			];
263
-
264
-			$user = parse_url($subscription['source'], PHP_URL_USER);
265
-			$pass = parse_url($subscription['source'], PHP_URL_PASS);
266
-			if ($user !== null && $pass !== null) {
267
-				$params['auth'] = [$user, $pass];
268
-			}
269
-
270
-			$response = $client->get($url, $params);
271
-			$body = $response->getBody();
272
-
273
-			if ($latestLocation) {
274
-				$mutations['{http://calendarserver.org/ns/}source'] = new Href($latestLocation);
275
-			}
276
-
277
-			$contentType = $response->getHeader('Content-Type');
278
-			$contentType = explode(';', $contentType, 2)[0];
279
-			switch($contentType) {
280
-				case 'application/calendar+json':
281
-					try {
282
-						$jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
283
-					} catch(Exception $ex) {
284
-						// In case of a parsing error return null
285
-						$this->logger->debug("Subscription $subscriptionId could not be parsed");
286
-						return null;
287
-					}
288
-					return $jCalendar->serialize();
289
-
290
-				case 'application/calendar+xml':
291
-					try {
292
-						$xCalendar = Reader::readXML($body);
293
-					} catch(Exception $ex) {
294
-						// In case of a parsing error return null
295
-						$this->logger->debug("Subscription $subscriptionId could not be parsed");
296
-						return null;
297
-					}
298
-					return $xCalendar->serialize();
299
-
300
-				case 'text/calendar':
301
-				default:
302
-					try {
303
-						$vCalendar = Reader::read($body);
304
-					} catch(Exception $ex) {
305
-						// In case of a parsing error return null
306
-						$this->logger->debug("Subscription $subscriptionId could not be parsed");
307
-						return null;
308
-					}
309
-					return $vCalendar->serialize();
310
-			}
311
-		} catch(Exception $ex) {
312
-			$this->logger->logException($ex);
313
-			$this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error");
314
-
315
-			return null;
316
-		}
317
-	}
318
-
319
-	/**
320
-	 * check if:
321
-	 *  - current subscription stores a refreshrate
322
-	 *  - the webcal feed suggests a refreshrate
323
-	 *  - return suggested refreshrate if user didn't set a custom one
324
-	 *
325
-	 * @param array $subscription
326
-	 * @param string $webcalData
327
-	 * @return string|null
328
-	 */
329
-	private function checkWebcalDataForRefreshRate($subscription, $webcalData) {
330
-		// if there is no refreshrate stored in the database, check the webcal feed
331
-		// whether it suggests any refresh rate and store that in the database
332
-		if (isset($subscription[self::REFRESH_RATE]) && $subscription[self::REFRESH_RATE] !== null) {
333
-			return null;
334
-		}
335
-
336
-		/** @var Component\VCalendar $vCalendar */
337
-		$vCalendar = Reader::read($webcalData);
338
-
339
-		$newRefreshRate = null;
340
-		if (isset($vCalendar->{'X-PUBLISHED-TTL'})) {
341
-			$newRefreshRate = $vCalendar->{'X-PUBLISHED-TTL'}->getValue();
342
-		}
343
-		if (isset($vCalendar->{'REFRESH-INTERVAL'})) {
344
-			$newRefreshRate = $vCalendar->{'REFRESH-INTERVAL'}->getValue();
345
-		}
346
-
347
-		if (!$newRefreshRate) {
348
-			return null;
349
-		}
350
-
351
-		// check if new refresh rate is even valid
352
-		try {
353
-			DateTimeParser::parseDuration($newRefreshRate);
354
-		} catch(InvalidDataException $ex) {
355
-			return null;
356
-		}
357
-
358
-		return $newRefreshRate;
359
-	}
360
-
361
-	/**
362
-	 * update subscription stored in database
363
-	 * used to set:
364
-	 *  - refreshrate
365
-	 *  - source
366
-	 *
367
-	 * @param array $subscription
368
-	 * @param array $mutations
369
-	 */
370
-	private function updateSubscription(array $subscription, array $mutations) {
371
-		if (empty($mutations)) {
372
-			return;
373
-		}
374
-
375
-		$propPatch = new PropPatch($mutations);
376
-		$this->calDavBackend->updateSubscription($subscription['id'], $propPatch);
377
-		$propPatch->commit();
378
-	}
379
-
380
-	/**
381
-	 * This method will strip authentication information and replace the
382
-	 * 'webcal' or 'webcals' protocol scheme
383
-	 *
384
-	 * @param string $url
385
-	 * @return string|null
386
-	 */
387
-	private function cleanURL(string $url) {
388
-		$parsed = parse_url($url);
389
-		if ($parsed === false) {
390
-			return null;
391
-		}
392
-
393
-		if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
394
-			$scheme = 'http';
395
-		} else {
396
-			$scheme = 'https';
397
-		}
398
-
399
-		$host = $parsed['host'] ?? '';
400
-		$port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
401
-		$path = $parsed['path'] ?? '';
402
-		$query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
403
-		$fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
404
-
405
-		$cleanURL = "$scheme://$host$port$path$query$fragment";
406
-		// parse_url is giving some weird results if no url and no :// is given,
407
-		// so let's test the url again
408
-		$parsedClean = parse_url($cleanURL);
409
-		if ($parsedClean === false || !isset($parsedClean['host'])) {
410
-			return null;
411
-		}
412
-
413
-		return $cleanURL;
414
-	}
415
-
416
-	/**
417
-	 * Returns a random uri for a calendar-object
418
-	 *
419
-	 * @return string
420
-	 */
421
-	public function getRandomCalendarObjectUri():string {
422
-		return UUIDUtil::getUUID() . '.ics';
423
-	}
55
+    /** @var CalDavBackend */
56
+    private $calDavBackend;
57
+
58
+    /** @var IClientService */
59
+    private $clientService;
60
+
61
+    /** @var IConfig */
62
+    private $config;
63
+
64
+    /** @var ILogger */
65
+    private $logger;
66
+
67
+    public const REFRESH_RATE = '{http://apple.com/ns/ical/}refreshrate';
68
+    public const STRIP_ALARMS = '{http://calendarserver.org/ns/}subscribed-strip-alarms';
69
+    public const STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments';
70
+    public const STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos';
71
+
72
+    /**
73
+     * RefreshWebcalJob constructor.
74
+     *
75
+     * @param CalDavBackend $calDavBackend
76
+     * @param IClientService $clientService
77
+     * @param IConfig $config
78
+     * @param ILogger $logger
79
+     */
80
+    public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, ILogger $logger) {
81
+        $this->calDavBackend = $calDavBackend;
82
+        $this->clientService = $clientService;
83
+        $this->config = $config;
84
+        $this->logger = $logger;
85
+    }
86
+
87
+    /**
88
+     * @param string $principalUri
89
+     * @param string $uri
90
+     */
91
+    public function refreshSubscription(string $principalUri, string $uri) {
92
+        $subscription = $this->getSubscription($principalUri, $uri);
93
+        $mutations = [];
94
+        if (!$subscription) {
95
+            return;
96
+        }
97
+
98
+        $webcalData = $this->queryWebcalFeed($subscription, $mutations);
99
+        if (!$webcalData) {
100
+            return;
101
+        }
102
+
103
+        $stripTodos = ($subscription[self::STRIP_TODOS] ?? 1) === 1;
104
+        $stripAlarms = ($subscription[self::STRIP_ALARMS] ?? 1) === 1;
105
+        $stripAttachments = ($subscription[self::STRIP_ATTACHMENTS] ?? 1) === 1;
106
+
107
+        try {
108
+            $splitter = new ICalendar($webcalData, Reader::OPTION_FORGIVING);
109
+
110
+            // we wait with deleting all outdated events till we parsed the new ones
111
+            // in case the new calendar is broken and `new ICalendar` throws a ParseException
112
+            // the user will still see the old data
113
+            $this->calDavBackend->purgeAllCachedEventsForSubscription($subscription['id']);
114
+
115
+            while ($vObject = $splitter->getNext()) {
116
+                /** @var Component $vObject */
117
+                $compName = null;
118
+
119
+                foreach ($vObject->getComponents() as $component) {
120
+                    if ($component->name === 'VTIMEZONE') {
121
+                        continue;
122
+                    }
123
+
124
+                    $compName = $component->name;
125
+
126
+                    if ($stripAlarms) {
127
+                        unset($component->{'VALARM'});
128
+                    }
129
+                    if ($stripAttachments) {
130
+                        unset($component->{'ATTACH'});
131
+                    }
132
+                }
133
+
134
+                if ($stripTodos && $compName === 'VTODO') {
135
+                    continue;
136
+                }
137
+
138
+                $uri = $this->getRandomCalendarObjectUri();
139
+                $calendarData = $vObject->serialize();
140
+                try {
141
+                    $this->calDavBackend->createCalendarObject($subscription['id'], $uri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
142
+                } catch(BadRequest $ex) {
143
+                    $this->logger->logException($ex);
144
+                }
145
+            }
146
+
147
+            $newRefreshRate = $this->checkWebcalDataForRefreshRate($subscription, $webcalData);
148
+            if ($newRefreshRate) {
149
+                $mutations[self::REFRESH_RATE] = $newRefreshRate;
150
+            }
151
+
152
+            $this->updateSubscription($subscription, $mutations);
153
+        } catch(ParseException $ex) {
154
+            $subscriptionId = $subscription['id'];
155
+
156
+            $this->logger->logException($ex);
157
+            $this->logger->warning("Subscription $subscriptionId could not be refreshed due to a parsing error");
158
+        }
159
+    }
160
+
161
+    /**
162
+     * loads subscription from backend
163
+     *
164
+     * @param string $principalUri
165
+     * @param string $uri
166
+     * @return array|null
167
+     */
168
+    public function getSubscription(string $principalUri, string $uri) {
169
+        $subscriptions = array_values(array_filter(
170
+            $this->calDavBackend->getSubscriptionsForUser($principalUri),
171
+            function($sub) use ($uri) {
172
+                return $sub['uri'] === $uri;
173
+            }
174
+        ));
175
+
176
+        if (count($subscriptions) === 0) {
177
+            return null;
178
+        }
179
+
180
+        return $subscriptions[0];
181
+    }
182
+
183
+    /**
184
+     * gets webcal feed from remote server
185
+     *
186
+     * @param array $subscription
187
+     * @param array &$mutations
188
+     * @return null|string
189
+     */
190
+    private function queryWebcalFeed(array $subscription, array &$mutations) {
191
+        $client = $this->clientService->newClient();
192
+
193
+        $didBreak301Chain = false;
194
+        $latestLocation = null;
195
+
196
+        $handlerStack = HandlerStack::create();
197
+        $handlerStack->push(Middleware::mapRequest(function (RequestInterface $request) {
198
+            return $request
199
+                ->withHeader('Accept', 'text/calendar, application/calendar+json, application/calendar+xml')
200
+                ->withHeader('User-Agent', 'Nextcloud Webcal Crawler');
201
+        }));
202
+        $handlerStack->push(Middleware::mapResponse(function(ResponseInterface $response) use (&$didBreak301Chain, &$latestLocation) {
203
+            if (!$didBreak301Chain) {
204
+                if ($response->getStatusCode() !== 301) {
205
+                    $didBreak301Chain = true;
206
+                } else {
207
+                    $latestLocation = $response->getHeader('Location');
208
+                }
209
+            }
210
+            return $response;
211
+        }));
212
+
213
+        $allowLocalAccess = $this->config->getAppValue('dav', 'webcalAllowLocalAccess', 'no');
214
+        $subscriptionId = $subscription['id'];
215
+        $url = $this->cleanURL($subscription['source']);
216
+        if ($url === null) {
217
+            return null;
218
+        }
219
+
220
+        if ($allowLocalAccess !== 'yes') {
221
+            $host = strtolower(parse_url($url, PHP_URL_HOST));
222
+            // remove brackets from IPv6 addresses
223
+            if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
224
+                $host = substr($host, 1, -1);
225
+            }
226
+
227
+            // Disallow localhost and local network
228
+            if ($host === 'localhost' || substr($host, -6) === '.local' || substr($host, -10) === '.localhost') {
229
+                $this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
230
+                return null;
231
+            }
232
+
233
+            // Disallow hostname only
234
+            if (substr_count($host, '.') === 0) {
235
+                $this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
236
+                return null;
237
+            }
238
+
239
+            if ((bool)filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
240
+                $this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
241
+                return null;
242
+            }
243
+
244
+            // Also check for IPv6 IPv4 nesting, because that's not covered by filter_var
245
+            if ((bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
246
+                $delimiter = strrpos($host, ':'); // Get last colon
247
+                $ipv4Address = substr($host, $delimiter + 1);
248
+
249
+                if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
250
+                    $this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
251
+                    return null;
252
+                }
253
+            }
254
+        }
255
+
256
+        try {
257
+            $params = [
258
+                'allow_redirects' => [
259
+                    'redirects' => 10
260
+                ],
261
+                'handler' => $handlerStack,
262
+            ];
263
+
264
+            $user = parse_url($subscription['source'], PHP_URL_USER);
265
+            $pass = parse_url($subscription['source'], PHP_URL_PASS);
266
+            if ($user !== null && $pass !== null) {
267
+                $params['auth'] = [$user, $pass];
268
+            }
269
+
270
+            $response = $client->get($url, $params);
271
+            $body = $response->getBody();
272
+
273
+            if ($latestLocation) {
274
+                $mutations['{http://calendarserver.org/ns/}source'] = new Href($latestLocation);
275
+            }
276
+
277
+            $contentType = $response->getHeader('Content-Type');
278
+            $contentType = explode(';', $contentType, 2)[0];
279
+            switch($contentType) {
280
+                case 'application/calendar+json':
281
+                    try {
282
+                        $jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
283
+                    } catch(Exception $ex) {
284
+                        // In case of a parsing error return null
285
+                        $this->logger->debug("Subscription $subscriptionId could not be parsed");
286
+                        return null;
287
+                    }
288
+                    return $jCalendar->serialize();
289
+
290
+                case 'application/calendar+xml':
291
+                    try {
292
+                        $xCalendar = Reader::readXML($body);
293
+                    } catch(Exception $ex) {
294
+                        // In case of a parsing error return null
295
+                        $this->logger->debug("Subscription $subscriptionId could not be parsed");
296
+                        return null;
297
+                    }
298
+                    return $xCalendar->serialize();
299
+
300
+                case 'text/calendar':
301
+                default:
302
+                    try {
303
+                        $vCalendar = Reader::read($body);
304
+                    } catch(Exception $ex) {
305
+                        // In case of a parsing error return null
306
+                        $this->logger->debug("Subscription $subscriptionId could not be parsed");
307
+                        return null;
308
+                    }
309
+                    return $vCalendar->serialize();
310
+            }
311
+        } catch(Exception $ex) {
312
+            $this->logger->logException($ex);
313
+            $this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error");
314
+
315
+            return null;
316
+        }
317
+    }
318
+
319
+    /**
320
+     * check if:
321
+     *  - current subscription stores a refreshrate
322
+     *  - the webcal feed suggests a refreshrate
323
+     *  - return suggested refreshrate if user didn't set a custom one
324
+     *
325
+     * @param array $subscription
326
+     * @param string $webcalData
327
+     * @return string|null
328
+     */
329
+    private function checkWebcalDataForRefreshRate($subscription, $webcalData) {
330
+        // if there is no refreshrate stored in the database, check the webcal feed
331
+        // whether it suggests any refresh rate and store that in the database
332
+        if (isset($subscription[self::REFRESH_RATE]) && $subscription[self::REFRESH_RATE] !== null) {
333
+            return null;
334
+        }
335
+
336
+        /** @var Component\VCalendar $vCalendar */
337
+        $vCalendar = Reader::read($webcalData);
338
+
339
+        $newRefreshRate = null;
340
+        if (isset($vCalendar->{'X-PUBLISHED-TTL'})) {
341
+            $newRefreshRate = $vCalendar->{'X-PUBLISHED-TTL'}->getValue();
342
+        }
343
+        if (isset($vCalendar->{'REFRESH-INTERVAL'})) {
344
+            $newRefreshRate = $vCalendar->{'REFRESH-INTERVAL'}->getValue();
345
+        }
346
+
347
+        if (!$newRefreshRate) {
348
+            return null;
349
+        }
350
+
351
+        // check if new refresh rate is even valid
352
+        try {
353
+            DateTimeParser::parseDuration($newRefreshRate);
354
+        } catch(InvalidDataException $ex) {
355
+            return null;
356
+        }
357
+
358
+        return $newRefreshRate;
359
+    }
360
+
361
+    /**
362
+     * update subscription stored in database
363
+     * used to set:
364
+     *  - refreshrate
365
+     *  - source
366
+     *
367
+     * @param array $subscription
368
+     * @param array $mutations
369
+     */
370
+    private function updateSubscription(array $subscription, array $mutations) {
371
+        if (empty($mutations)) {
372
+            return;
373
+        }
374
+
375
+        $propPatch = new PropPatch($mutations);
376
+        $this->calDavBackend->updateSubscription($subscription['id'], $propPatch);
377
+        $propPatch->commit();
378
+    }
379
+
380
+    /**
381
+     * This method will strip authentication information and replace the
382
+     * 'webcal' or 'webcals' protocol scheme
383
+     *
384
+     * @param string $url
385
+     * @return string|null
386
+     */
387
+    private function cleanURL(string $url) {
388
+        $parsed = parse_url($url);
389
+        if ($parsed === false) {
390
+            return null;
391
+        }
392
+
393
+        if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
394
+            $scheme = 'http';
395
+        } else {
396
+            $scheme = 'https';
397
+        }
398
+
399
+        $host = $parsed['host'] ?? '';
400
+        $port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
401
+        $path = $parsed['path'] ?? '';
402
+        $query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
403
+        $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
404
+
405
+        $cleanURL = "$scheme://$host$port$path$query$fragment";
406
+        // parse_url is giving some weird results if no url and no :// is given,
407
+        // so let's test the url again
408
+        $parsedClean = parse_url($cleanURL);
409
+        if ($parsedClean === false || !isset($parsedClean['host'])) {
410
+            return null;
411
+        }
412
+
413
+        return $cleanURL;
414
+    }
415
+
416
+    /**
417
+     * Returns a random uri for a calendar-object
418
+     *
419
+     * @return string
420
+     */
421
+    public function getRandomCalendarObjectUri():string {
422
+        return UUIDUtil::getUUID() . '.ics';
423
+    }
424 424
 }
Please login to merge, or discard this patch.
Spacing   +15 added lines, -15 removed lines patch added patch discarded remove patch
@@ -139,7 +139,7 @@  discard block
 block discarded – undo
139 139
 				$calendarData = $vObject->serialize();
140 140
 				try {
141 141
 					$this->calDavBackend->createCalendarObject($subscription['id'], $uri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
142
-				} catch(BadRequest $ex) {
142
+				} catch (BadRequest $ex) {
143 143
 					$this->logger->logException($ex);
144 144
 				}
145 145
 			}
@@ -150,7 +150,7 @@  discard block
 block discarded – undo
150 150
 			}
151 151
 
152 152
 			$this->updateSubscription($subscription, $mutations);
153
-		} catch(ParseException $ex) {
153
+		} catch (ParseException $ex) {
154 154
 			$subscriptionId = $subscription['id'];
155 155
 
156 156
 			$this->logger->logException($ex);
@@ -194,7 +194,7 @@  discard block
 block discarded – undo
194 194
 		$latestLocation = null;
195 195
 
196 196
 		$handlerStack = HandlerStack::create();
197
-		$handlerStack->push(Middleware::mapRequest(function (RequestInterface $request) {
197
+		$handlerStack->push(Middleware::mapRequest(function(RequestInterface $request) {
198 198
 			return $request
199 199
 				->withHeader('Accept', 'text/calendar, application/calendar+json, application/calendar+xml')
200 200
 				->withHeader('User-Agent', 'Nextcloud Webcal Crawler');
@@ -236,13 +236,13 @@  discard block
 block discarded – undo
236 236
 				return null;
237 237
 			}
238 238
 
239
-			if ((bool)filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
239
+			if ((bool) filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
240 240
 				$this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules");
241 241
 				return null;
242 242
 			}
243 243
 
244 244
 			// Also check for IPv6 IPv4 nesting, because that's not covered by filter_var
245
-			if ((bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
245
+			if ((bool) filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
246 246
 				$delimiter = strrpos($host, ':'); // Get last colon
247 247
 				$ipv4Address = substr($host, $delimiter + 1);
248 248
 
@@ -276,11 +276,11 @@  discard block
 block discarded – undo
276 276
 
277 277
 			$contentType = $response->getHeader('Content-Type');
278 278
 			$contentType = explode(';', $contentType, 2)[0];
279
-			switch($contentType) {
279
+			switch ($contentType) {
280 280
 				case 'application/calendar+json':
281 281
 					try {
282 282
 						$jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
283
-					} catch(Exception $ex) {
283
+					} catch (Exception $ex) {
284 284
 						// In case of a parsing error return null
285 285
 						$this->logger->debug("Subscription $subscriptionId could not be parsed");
286 286
 						return null;
@@ -290,7 +290,7 @@  discard block
 block discarded – undo
290 290
 				case 'application/calendar+xml':
291 291
 					try {
292 292
 						$xCalendar = Reader::readXML($body);
293
-					} catch(Exception $ex) {
293
+					} catch (Exception $ex) {
294 294
 						// In case of a parsing error return null
295 295
 						$this->logger->debug("Subscription $subscriptionId could not be parsed");
296 296
 						return null;
@@ -301,14 +301,14 @@  discard block
 block discarded – undo
301 301
 				default:
302 302
 					try {
303 303
 						$vCalendar = Reader::read($body);
304
-					} catch(Exception $ex) {
304
+					} catch (Exception $ex) {
305 305
 						// In case of a parsing error return null
306 306
 						$this->logger->debug("Subscription $subscriptionId could not be parsed");
307 307
 						return null;
308 308
 					}
309 309
 					return $vCalendar->serialize();
310 310
 			}
311
-		} catch(Exception $ex) {
311
+		} catch (Exception $ex) {
312 312
 			$this->logger->logException($ex);
313 313
 			$this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error");
314 314
 
@@ -351,7 +351,7 @@  discard block
 block discarded – undo
351 351
 		// check if new refresh rate is even valid
352 352
 		try {
353 353
 			DateTimeParser::parseDuration($newRefreshRate);
354
-		} catch(InvalidDataException $ex) {
354
+		} catch (InvalidDataException $ex) {
355 355
 			return null;
356 356
 		}
357 357
 
@@ -397,10 +397,10 @@  discard block
 block discarded – undo
397 397
 		}
398 398
 
399 399
 		$host = $parsed['host'] ?? '';
400
-		$port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
400
+		$port = isset($parsed['port']) ? ':'.$parsed['port'] : '';
401 401
 		$path = $parsed['path'] ?? '';
402
-		$query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
403
-		$fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
402
+		$query = isset($parsed['query']) ? '?'.$parsed['query'] : '';
403
+		$fragment = isset($parsed['fragment']) ? '#'.$parsed['fragment'] : '';
404 404
 
405 405
 		$cleanURL = "$scheme://$host$port$path$query$fragment";
406 406
 		// parse_url is giving some weird results if no url and no :// is given,
@@ -419,6 +419,6 @@  discard block
 block discarded – undo
419 419
 	 * @return string
420 420
 	 */
421 421
 	public function getRandomCalendarObjectUri():string {
422
-		return UUIDUtil::getUUID() . '.ics';
422
+		return UUIDUtil::getUUID().'.ics';
423 423
 	}
424 424
 }
Please login to merge, or discard this patch.