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