Passed
Push — master ( f97491...570c25 )
by Roeland
14:12 queued 10s
created
apps/dav/lib/CalDAV/BirthdayService.php 2 patches
Indentation   +436 added lines, -436 removed lines patch added patch discarded remove patch
@@ -51,440 +51,440 @@
 block discarded – undo
51 51
  * @package OCA\DAV\CalDAV
52 52
  */
53 53
 class BirthdayService {
54
-	public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
55
-
56
-	/** @var GroupPrincipalBackend */
57
-	private $principalBackend;
58
-
59
-	/** @var CalDavBackend  */
60
-	private $calDavBackEnd;
61
-
62
-	/** @var CardDavBackend  */
63
-	private $cardDavBackEnd;
64
-
65
-	/** @var IConfig */
66
-	private $config;
67
-
68
-	/** @var IDBConnection */
69
-	private $dbConnection;
70
-
71
-	/** @var IL10N */
72
-	private $l10n;
73
-
74
-	/**
75
-	 * BirthdayService constructor.
76
-	 *
77
-	 * @param CalDavBackend $calDavBackEnd
78
-	 * @param CardDavBackend $cardDavBackEnd
79
-	 * @param GroupPrincipalBackend $principalBackend
80
-	 * @param IConfig $config
81
-	 * @param IDBConnection $dbConnection
82
-	 * @param IL10N $l10n
83
-	 */
84
-	public function __construct(CalDavBackend $calDavBackEnd,
85
-								CardDavBackend $cardDavBackEnd,
86
-								GroupPrincipalBackend $principalBackend,
87
-								IConfig $config,
88
-								IDBConnection $dbConnection,
89
-								IL10N $l10n) {
90
-		$this->calDavBackEnd = $calDavBackEnd;
91
-		$this->cardDavBackEnd = $cardDavBackEnd;
92
-		$this->principalBackend = $principalBackend;
93
-		$this->config = $config;
94
-		$this->dbConnection = $dbConnection;
95
-		$this->l10n = $l10n;
96
-	}
97
-
98
-	/**
99
-	 * @param int $addressBookId
100
-	 * @param string $cardUri
101
-	 * @param string $cardData
102
-	 */
103
-	public function onCardChanged(int $addressBookId,
104
-								  string $cardUri,
105
-								  string $cardData) {
106
-		if (!$this->isGloballyEnabled()) {
107
-			return;
108
-		}
109
-
110
-		$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
111
-		$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
112
-		$targetPrincipals[] = $book['principaluri'];
113
-		$datesToSync = [
114
-			['postfix' => '', 'field' => 'BDAY'],
115
-			['postfix' => '-death', 'field' => 'DEATHDATE'],
116
-			['postfix' => '-anniversary', 'field' => 'ANNIVERSARY'],
117
-		];
118
-
119
-		foreach ($targetPrincipals as $principalUri) {
120
-			if (!$this->isUserEnabled($principalUri)) {
121
-				continue;
122
-			}
123
-
124
-			$calendar = $this->ensureCalendarExists($principalUri);
125
-			foreach ($datesToSync as $type) {
126
-				$this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type);
127
-			}
128
-		}
129
-	}
130
-
131
-	/**
132
-	 * @param int $addressBookId
133
-	 * @param string $cardUri
134
-	 */
135
-	public function onCardDeleted(int $addressBookId,
136
-								  string $cardUri) {
137
-		if (!$this->isGloballyEnabled()) {
138
-			return;
139
-		}
140
-
141
-		$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
142
-		$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
143
-		$targetPrincipals[] = $book['principaluri'];
144
-		foreach ($targetPrincipals as $principalUri) {
145
-			if (!$this->isUserEnabled($principalUri)) {
146
-				continue;
147
-			}
148
-
149
-			$calendar = $this->ensureCalendarExists($principalUri);
150
-			foreach (['', '-death', '-anniversary'] as $tag) {
151
-				$objectUri = $book['uri'] . '-' . $cardUri . $tag .'.ics';
152
-				$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri);
153
-			}
154
-		}
155
-	}
156
-
157
-	/**
158
-	 * @param string $principal
159
-	 * @return array|null
160
-	 * @throws \Sabre\DAV\Exception\BadRequest
161
-	 */
162
-	public function ensureCalendarExists(string $principal):?array {
163
-		$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
164
-		if (!is_null($calendar)) {
165
-			return $calendar;
166
-		}
167
-		$this->calDavBackEnd->createCalendar($principal, self::BIRTHDAY_CALENDAR_URI, [
168
-			'{DAV:}displayname' => $this->l10n->t('Contact birthdays'),
169
-			'{http://apple.com/ns/ical/}calendar-color' => '#E9D859',
170
-			'components' => 'VEVENT',
171
-		]);
172
-
173
-		return $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
174
-	}
175
-
176
-	/**
177
-	 * @param $cardData
178
-	 * @param $dateField
179
-	 * @param $postfix
180
-	 * @return VCalendar|null
181
-	 * @throws InvalidDataException
182
-	 */
183
-	public function buildDateFromContact(string $cardData,
184
-										 string $dateField,
185
-										 string $postfix):?VCalendar {
186
-		if (empty($cardData)) {
187
-			return null;
188
-		}
189
-		try {
190
-			$doc = Reader::read($cardData);
191
-			// We're always converting to vCard 4.0 so we can rely on the
192
-			// VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
193
-			if (!$doc instanceof VCard) {
194
-				return null;
195
-			}
196
-			$doc = $doc->convert(Document::VCARD40);
197
-		} catch (Exception $e) {
198
-			return null;
199
-		}
200
-
201
-		if (!isset($doc->{$dateField})) {
202
-			return null;
203
-		}
204
-		if (!isset($doc->FN)) {
205
-			return null;
206
-		}
207
-		$birthday = $doc->{$dateField};
208
-		if (!(string)$birthday) {
209
-			return null;
210
-		}
211
-		// Skip if the BDAY property is not of the right type.
212
-		if (!$birthday instanceof DateAndOrTime) {
213
-			return null;
214
-		}
215
-
216
-		// Skip if we can't parse the BDAY value.
217
-		try {
218
-			$dateParts = DateTimeParser::parseVCardDateTime($birthday->getValue());
219
-		} catch (InvalidDataException $e) {
220
-			return null;
221
-		}
222
-
223
-		$unknownYear = false;
224
-		$originalYear = null;
225
-		if (!$dateParts['year']) {
226
-			$birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
227
-
228
-			$unknownYear = true;
229
-		} else {
230
-			$parameters = $birthday->parameters();
231
-			if (isset($parameters['X-APPLE-OMIT-YEAR'])) {
232
-				$omitYear = $parameters['X-APPLE-OMIT-YEAR'];
233
-				if ($dateParts['year'] === $omitYear) {
234
-					$birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
235
-					$unknownYear = true;
236
-				}
237
-			} else {
238
-				$originalYear = (int)$dateParts['year'];
239
-				// 'X-APPLE-OMIT-YEAR' is not always present, at least iOS 12.4 uses the hard coded date of 1604 (the start of the gregorian calendar) when the year is unknown
240
-				if ($originalYear == 1604) {
241
-					$originalYear = null;
242
-					$unknownYear = true;
243
-					$birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
244
-				}
245
-				if ($originalYear < 1970) {
246
-					$birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
247
-				}
248
-			}
249
-		}
250
-
251
-		try {
252
-			if ($birthday instanceof DateAndOrTime) {
253
-				$date = $birthday->getDateTime();
254
-			} else {
255
-				$date = new \DateTimeImmutable($birthday);
256
-			}
257
-		} catch (Exception $e) {
258
-			return null;
259
-		}
260
-
261
-		$summary = $this->formatTitle($dateField, $doc->FN->getValue(), $originalYear, $this->dbConnection->supports4ByteText());
262
-
263
-		$vCal = new VCalendar();
264
-		$vCal->VERSION = '2.0';
265
-		$vCal->PRODID = '-//IDN nextcloud.com//Birthday calendar//EN';
266
-		$vEvent = $vCal->createComponent('VEVENT');
267
-		$vEvent->add('DTSTART');
268
-		$vEvent->DTSTART->setDateTime(
269
-			$date
270
-		);
271
-		$vEvent->DTSTART['VALUE'] = 'DATE';
272
-		$vEvent->add('DTEND');
273
-
274
-		$dtEndDate = (new \DateTime())->setTimestamp($date->getTimeStamp());
275
-		$dtEndDate->add(new \DateInterval('P1D'));
276
-		$vEvent->DTEND->setDateTime(
277
-			$dtEndDate
278
-		);
279
-
280
-		$vEvent->DTEND['VALUE'] = 'DATE';
281
-		$vEvent->{'UID'} = $doc->UID . $postfix;
282
-		$vEvent->{'RRULE'} = 'FREQ=YEARLY';
283
-		$vEvent->{'SUMMARY'} = $summary;
284
-		$vEvent->{'TRANSP'} = 'TRANSPARENT';
285
-		$vEvent->{'X-NEXTCLOUD-BC-FIELD-TYPE'} = $dateField;
286
-		$vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $unknownYear ? '1' : '0';
287
-		if ($originalYear !== null) {
288
-			$vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string) $originalYear;
289
-		}
290
-		$alarm = $vCal->createComponent('VALARM');
291
-		$alarm->add($vCal->createProperty('TRIGGER', '-PT0M', ['VALUE' => 'DURATION']));
292
-		$alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
293
-		$alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
294
-		$vEvent->add($alarm);
295
-		$vCal->add($vEvent);
296
-		return $vCal;
297
-	}
298
-
299
-	/**
300
-	 * @param string $user
301
-	 */
302
-	public function resetForUser(string $user):void {
303
-		$principal = 'principals/users/'.$user;
304
-		$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
305
-		$calendarObjects = $this->calDavBackEnd->getCalendarObjects($calendar['id'], CalDavBackend::CALENDAR_TYPE_CALENDAR);
306
-
307
-		foreach ($calendarObjects as $calendarObject) {
308
-			$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $calendarObject['uri'], CalDavBackend::CALENDAR_TYPE_CALENDAR);
309
-		}
310
-	}
311
-
312
-	/**
313
-	 * @param string $user
314
-	 * @throws \Sabre\DAV\Exception\BadRequest
315
-	 */
316
-	public function syncUser(string $user):void {
317
-		$principal = 'principals/users/'.$user;
318
-		$this->ensureCalendarExists($principal);
319
-		$books = $this->cardDavBackEnd->getAddressBooksForUser($principal);
320
-		foreach ($books as $book) {
321
-			$cards = $this->cardDavBackEnd->getCards($book['id']);
322
-			foreach ($cards as $card) {
323
-				$this->onCardChanged((int) $book['id'], $card['uri'], $card['carddata']);
324
-			}
325
-		}
326
-	}
327
-
328
-	/**
329
-	 * @param string $existingCalendarData
330
-	 * @param VCalendar $newCalendarData
331
-	 * @return bool
332
-	 */
333
-	public function birthdayEvenChanged(string $existingCalendarData,
334
-										VCalendar $newCalendarData):bool {
335
-		try {
336
-			$existingBirthday = Reader::read($existingCalendarData);
337
-		} catch (Exception $ex) {
338
-			return true;
339
-		}
340
-
341
-		return (
342
-			$newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() ||
343
-			$newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
344
-		);
345
-	}
346
-
347
-	/**
348
-	 * @param integer $addressBookId
349
-	 * @return mixed
350
-	 */
351
-	protected function getAllAffectedPrincipals(int $addressBookId) {
352
-		$targetPrincipals = [];
353
-		$shares = $this->cardDavBackEnd->getShares($addressBookId);
354
-		foreach ($shares as $share) {
355
-			if ($share['{http://owncloud.org/ns}group-share']) {
356
-				$users = $this->principalBackend->getGroupMemberSet($share['{http://owncloud.org/ns}principal']);
357
-				foreach ($users as $user) {
358
-					$targetPrincipals[] = $user['uri'];
359
-				}
360
-			} else {
361
-				$targetPrincipals[] = $share['{http://owncloud.org/ns}principal'];
362
-			}
363
-		}
364
-		return array_values(array_unique($targetPrincipals, SORT_STRING));
365
-	}
366
-
367
-	/**
368
-	 * @param string $cardUri
369
-	 * @param string $cardData
370
-	 * @param array $book
371
-	 * @param int $calendarId
372
-	 * @param array $type
373
-	 * @throws InvalidDataException
374
-	 * @throws \Sabre\DAV\Exception\BadRequest
375
-	 */
376
-	private function updateCalendar(string $cardUri,
377
-									string $cardData,
378
-									array $book,
379
-									int $calendarId,
380
-									array $type):void {
381
-		$objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics';
382
-		$calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix']);
383
-		$existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri);
384
-		if (is_null($calendarData)) {
385
-			if (!is_null($existing)) {
386
-				$this->calDavBackEnd->deleteCalendarObject($calendarId, $objectUri);
387
-			}
388
-		} else {
389
-			if (is_null($existing)) {
390
-				$this->calDavBackEnd->createCalendarObject($calendarId, $objectUri, $calendarData->serialize());
391
-			} else {
392
-				if ($this->birthdayEvenChanged($existing['calendardata'], $calendarData)) {
393
-					$this->calDavBackEnd->updateCalendarObject($calendarId, $objectUri, $calendarData->serialize());
394
-				}
395
-			}
396
-		}
397
-	}
398
-
399
-	/**
400
-	 * checks if the admin opted-out of birthday calendars
401
-	 *
402
-	 * @return bool
403
-	 */
404
-	private function isGloballyEnabled():bool {
405
-		return $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes') === 'yes';
406
-	}
407
-
408
-	/**
409
-	 * Checks if the user opted-out of birthday calendars
410
-	 *
411
-	 * @param string $userPrincipal The user principal to check for
412
-	 * @return bool
413
-	 */
414
-	private function isUserEnabled(string $userPrincipal):bool {
415
-		if (strpos($userPrincipal, 'principals/users/') === 0) {
416
-			$userId = substr($userPrincipal, 17);
417
-			$isEnabled = $this->config->getUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes');
418
-			return $isEnabled === 'yes';
419
-		}
420
-
421
-		// not sure how we got here, just be on the safe side and return true
422
-		return true;
423
-	}
424
-
425
-	/**
426
-	 * Formats title of Birthday event
427
-	 *
428
-	 * @param string $field Field name like BDAY, ANNIVERSARY, ...
429
-	 * @param string $name Name of contact
430
-	 * @param int|null $year Year of birth, anniversary, ...
431
-	 * @param bool $supports4Byte Whether or not the database supports 4 byte chars
432
-	 * @return string The formatted title
433
-	 */
434
-	private function formatTitle(string $field,
435
-								 string $name,
436
-								 int $year = null,
437
-								 bool $supports4Byte = true):string {
438
-		if ($supports4Byte) {
439
-			switch ($field) {
440
-				case 'BDAY':
441
-					return implode('', [
442
-						'
Please login to merge, or discard this patch.
Spacing   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -148,7 +148,7 @@  discard block
 block discarded – undo
148 148
 
149 149
 			$calendar = $this->ensureCalendarExists($principalUri);
150 150
 			foreach (['', '-death', '-anniversary'] as $tag) {
151
-				$objectUri = $book['uri'] . '-' . $cardUri . $tag .'.ics';
151
+				$objectUri = $book['uri'].'-'.$cardUri.$tag.'.ics';
152 152
 				$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri);
153 153
 			}
154 154
 		}
@@ -159,7 +159,7 @@  discard block
 block discarded – undo
159 159
 	 * @return array|null
160 160
 	 * @throws \Sabre\DAV\Exception\BadRequest
161 161
 	 */
162
-	public function ensureCalendarExists(string $principal):?array {
162
+	public function ensureCalendarExists(string $principal): ?array {
163 163
 		$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
164 164
 		if (!is_null($calendar)) {
165 165
 			return $calendar;
@@ -182,7 +182,7 @@  discard block
 block discarded – undo
182 182
 	 */
183 183
 	public function buildDateFromContact(string $cardData,
184 184
 										 string $dateField,
185
-										 string $postfix):?VCalendar {
185
+										 string $postfix): ?VCalendar {
186 186
 		if (empty($cardData)) {
187 187
 			return null;
188 188
 		}
@@ -205,7 +205,7 @@  discard block
 block discarded – undo
205 205
 			return null;
206 206
 		}
207 207
 		$birthday = $doc->{$dateField};
208
-		if (!(string)$birthday) {
208
+		if (!(string) $birthday) {
209 209
 			return null;
210 210
 		}
211 211
 		// Skip if the BDAY property is not of the right type.
@@ -223,7 +223,7 @@  discard block
 block discarded – undo
223 223
 		$unknownYear = false;
224 224
 		$originalYear = null;
225 225
 		if (!$dateParts['year']) {
226
-			$birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
226
+			$birthday = '1970-'.$dateParts['month'].'-'.$dateParts['date'];
227 227
 
228 228
 			$unknownYear = true;
229 229
 		} else {
@@ -231,19 +231,19 @@  discard block
 block discarded – undo
231 231
 			if (isset($parameters['X-APPLE-OMIT-YEAR'])) {
232 232
 				$omitYear = $parameters['X-APPLE-OMIT-YEAR'];
233 233
 				if ($dateParts['year'] === $omitYear) {
234
-					$birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
234
+					$birthday = '1970-'.$dateParts['month'].'-'.$dateParts['date'];
235 235
 					$unknownYear = true;
236 236
 				}
237 237
 			} else {
238
-				$originalYear = (int)$dateParts['year'];
238
+				$originalYear = (int) $dateParts['year'];
239 239
 				// 'X-APPLE-OMIT-YEAR' is not always present, at least iOS 12.4 uses the hard coded date of 1604 (the start of the gregorian calendar) when the year is unknown
240 240
 				if ($originalYear == 1604) {
241 241
 					$originalYear = null;
242 242
 					$unknownYear = true;
243
-					$birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
243
+					$birthday = '1970-'.$dateParts['month'].'-'.$dateParts['date'];
244 244
 				}
245 245
 				if ($originalYear < 1970) {
246
-					$birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
246
+					$birthday = '1970-'.$dateParts['month'].'-'.$dateParts['date'];
247 247
 				}
248 248
 			}
249 249
 		}
@@ -278,7 +278,7 @@  discard block
 block discarded – undo
278 278
 		);
279 279
 
280 280
 		$vEvent->DTEND['VALUE'] = 'DATE';
281
-		$vEvent->{'UID'} = $doc->UID . $postfix;
281
+		$vEvent->{'UID'} = $doc->UID.$postfix;
282 282
 		$vEvent->{'RRULE'} = 'FREQ=YEARLY';
283 283
 		$vEvent->{'SUMMARY'} = $summary;
284 284
 		$vEvent->{'TRANSP'} = 'TRANSPARENT';
@@ -378,7 +378,7 @@  discard block
 block discarded – undo
378 378
 									array $book,
379 379
 									int $calendarId,
380 380
 									array $type):void {
381
-		$objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics';
381
+		$objectUri = $book['uri'].'-'.$cardUri.$type['postfix'].'.ics';
382 382
 		$calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix']);
383 383
 		$existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri);
384 384
 		if (is_null($calendarData)) {
@@ -441,20 +441,20 @@  discard block
 block discarded – undo
441 441
 					return implode('', [
442 442
 						'
Please login to merge, or discard this patch.