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