Completed
Push — master ( 400ade...d957f6 )
by Daniel
27:07
created
apps/dav/lib/CalDAV/BirthdayService.php 2 patches
Indentation   +459 added lines, -459 removed lines patch added patch discarded remove patch
@@ -29,464 +29,464 @@
 block discarded – undo
29 29
  * @package OCA\DAV\CalDAV
30 30
  */
31 31
 class BirthdayService {
32
-	public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
33
-	public const EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME = 'X-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR';
34
-
35
-	/**
36
-	 * BirthdayService constructor.
37
-	 */
38
-	public function __construct(
39
-		private CalDavBackend $calDavBackEnd,
40
-		private CardDavBackend $cardDavBackEnd,
41
-		private GroupPrincipalBackend $principalBackend,
42
-		private IConfig $config,
43
-		private IDBConnection $dbConnection,
44
-		private IL10N $l10n,
45
-	) {
46
-	}
47
-
48
-	public function onCardChanged(int $addressBookId,
49
-		string $cardUri,
50
-		string $cardData): void {
51
-		if (!$this->isGloballyEnabled()) {
52
-			return;
53
-		}
54
-
55
-		$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
56
-		$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
57
-		if ($book === null) {
58
-			return;
59
-		}
60
-		$targetPrincipals[] = $book['principaluri'];
61
-		$datesToSync = [
62
-			['postfix' => '', 'field' => 'BDAY'],
63
-			['postfix' => '-death', 'field' => 'DEATHDATE'],
64
-			['postfix' => '-anniversary', 'field' => 'ANNIVERSARY'],
65
-		];
66
-
67
-		foreach ($targetPrincipals as $principalUri) {
68
-			if (!$this->isUserEnabled($principalUri)) {
69
-				continue;
70
-			}
71
-
72
-			$reminderOffset = $this->getReminderOffsetForUser($principalUri);
73
-
74
-			$calendar = $this->ensureCalendarExists($principalUri);
75
-			if ($calendar === null) {
76
-				return;
77
-			}
78
-			foreach ($datesToSync as $type) {
79
-				$this->updateCalendar($cardUri, $cardData, $book, (int)$calendar['id'], $type, $reminderOffset);
80
-			}
81
-		}
82
-	}
83
-
84
-	public function onCardDeleted(int $addressBookId,
85
-		string $cardUri): void {
86
-		if (!$this->isGloballyEnabled()) {
87
-			return;
88
-		}
89
-
90
-		$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
91
-		$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
92
-		$targetPrincipals[] = $book['principaluri'];
93
-		foreach ($targetPrincipals as $principalUri) {
94
-			if (!$this->isUserEnabled($principalUri)) {
95
-				continue;
96
-			}
97
-
98
-			$calendar = $this->ensureCalendarExists($principalUri);
99
-			foreach (['', '-death', '-anniversary'] as $tag) {
100
-				$objectUri = $book['uri'] . '-' . $cardUri . $tag . '.ics';
101
-				$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
102
-			}
103
-		}
104
-	}
105
-
106
-	/**
107
-	 * @throws \Sabre\DAV\Exception\BadRequest
108
-	 */
109
-	public function ensureCalendarExists(string $principal): ?array {
110
-		$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
111
-		if (!is_null($calendar)) {
112
-			return $calendar;
113
-		}
114
-		$this->calDavBackEnd->createCalendar($principal, self::BIRTHDAY_CALENDAR_URI, [
115
-			'{DAV:}displayname' => $this->l10n->t('Contact birthdays'),
116
-			'{http://apple.com/ns/ical/}calendar-color' => '#E9D859',
117
-			'components' => 'VEVENT',
118
-		]);
119
-
120
-		return $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
121
-	}
122
-
123
-	/**
124
-	 * @param $cardData
125
-	 * @param $dateField
126
-	 * @param $postfix
127
-	 * @param $reminderOffset
128
-	 * @return VCalendar|null
129
-	 * @throws InvalidDataException
130
-	 */
131
-	public function buildDateFromContact(string $cardData,
132
-		string $dateField,
133
-		string $postfix,
134
-		?string $reminderOffset):?VCalendar {
135
-		if (empty($cardData)) {
136
-			return null;
137
-		}
138
-		try {
139
-			$doc = Reader::read($cardData);
140
-			// We're always converting to vCard 4.0 so we can rely on the
141
-			// VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
142
-			if (!$doc instanceof VCard) {
143
-				return null;
144
-			}
145
-			$doc = $doc->convert(Document::VCARD40);
146
-		} catch (Exception $e) {
147
-			return null;
148
-		}
149
-
150
-		if (isset($doc->{self::EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME})) {
151
-			return null;
152
-		}
153
-
154
-		if (!isset($doc->{$dateField})) {
155
-			return null;
156
-		}
157
-		if (!isset($doc->FN)) {
158
-			return null;
159
-		}
160
-		$birthday = $doc->{$dateField};
161
-		if (!(string)$birthday) {
162
-			return null;
163
-		}
164
-		// Skip if the BDAY property is not of the right type.
165
-		if (!$birthday instanceof DateAndOrTime) {
166
-			return null;
167
-		}
168
-
169
-		// Skip if we can't parse the BDAY value.
170
-		try {
171
-			$dateParts = DateTimeParser::parseVCardDateTime($birthday->getValue());
172
-		} catch (InvalidDataException $e) {
173
-			return null;
174
-		}
175
-		if ($dateParts['year'] !== null) {
176
-			$parameters = $birthday->parameters();
177
-			$omitYear = (isset($parameters['X-APPLE-OMIT-YEAR'])
178
-					&& $parameters['X-APPLE-OMIT-YEAR'] === $dateParts['year']);
179
-			// '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
180
-			if ($omitYear || (int)$dateParts['year'] === 1604) {
181
-				$dateParts['year'] = null;
182
-			}
183
-		}
184
-
185
-		$originalYear = null;
186
-		if ($dateParts['year'] !== null) {
187
-			$originalYear = (int)$dateParts['year'];
188
-		}
189
-
190
-		$leapDay = ((int)$dateParts['month'] === 2 && (int)$dateParts['date'] === 29);
191
-
192
-		if ($dateParts['year'] === null) {
193
-			$birthday = ($leapDay ? '1972-' : '1970-')
194
-				. $dateParts['month'] . '-' . $dateParts['date'];
195
-		}
196
-
197
-		try {
198
-			if ($birthday instanceof DateAndOrTime) {
199
-				$date = $birthday->getDateTime();
200
-			} else {
201
-				$date = new \DateTimeImmutable($birthday);
202
-			}
203
-		} catch (Exception $e) {
204
-			return null;
205
-		}
206
-
207
-		$summary = $this->formatTitle($dateField, $doc->FN->getValue(), $originalYear, $this->dbConnection->supports4ByteText());
208
-
209
-		$vCal = new VCalendar();
210
-		$vCal->VERSION = '2.0';
211
-		$vCal->PRODID = '-//IDN nextcloud.com//Birthday calendar//EN';
212
-		$vEvent = $vCal->createComponent('VEVENT');
213
-		$vEvent->add('DTSTART');
214
-		$vEvent->DTSTART->setDateTime(
215
-			$date
216
-		);
217
-		$vEvent->DTSTART['VALUE'] = 'DATE';
218
-		$vEvent->add('DTEND');
219
-
220
-		$dtEndDate = (new \DateTime())->setTimestamp($date->getTimeStamp());
221
-		$dtEndDate->add(new \DateInterval('P1D'));
222
-		$vEvent->DTEND->setDateTime(
223
-			$dtEndDate
224
-		);
225
-
226
-		$vEvent->DTEND['VALUE'] = 'DATE';
227
-		$vEvent->{'UID'} = $doc->UID . $postfix;
228
-		$vEvent->{'RRULE'} = 'FREQ=YEARLY';
229
-		if ($leapDay) {
230
-			/* Sabre\VObject supports BYMONTHDAY only if BYMONTH
32
+    public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
33
+    public const EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME = 'X-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR';
34
+
35
+    /**
36
+     * BirthdayService constructor.
37
+     */
38
+    public function __construct(
39
+        private CalDavBackend $calDavBackEnd,
40
+        private CardDavBackend $cardDavBackEnd,
41
+        private GroupPrincipalBackend $principalBackend,
42
+        private IConfig $config,
43
+        private IDBConnection $dbConnection,
44
+        private IL10N $l10n,
45
+    ) {
46
+    }
47
+
48
+    public function onCardChanged(int $addressBookId,
49
+        string $cardUri,
50
+        string $cardData): void {
51
+        if (!$this->isGloballyEnabled()) {
52
+            return;
53
+        }
54
+
55
+        $targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
56
+        $book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
57
+        if ($book === null) {
58
+            return;
59
+        }
60
+        $targetPrincipals[] = $book['principaluri'];
61
+        $datesToSync = [
62
+            ['postfix' => '', 'field' => 'BDAY'],
63
+            ['postfix' => '-death', 'field' => 'DEATHDATE'],
64
+            ['postfix' => '-anniversary', 'field' => 'ANNIVERSARY'],
65
+        ];
66
+
67
+        foreach ($targetPrincipals as $principalUri) {
68
+            if (!$this->isUserEnabled($principalUri)) {
69
+                continue;
70
+            }
71
+
72
+            $reminderOffset = $this->getReminderOffsetForUser($principalUri);
73
+
74
+            $calendar = $this->ensureCalendarExists($principalUri);
75
+            if ($calendar === null) {
76
+                return;
77
+            }
78
+            foreach ($datesToSync as $type) {
79
+                $this->updateCalendar($cardUri, $cardData, $book, (int)$calendar['id'], $type, $reminderOffset);
80
+            }
81
+        }
82
+    }
83
+
84
+    public function onCardDeleted(int $addressBookId,
85
+        string $cardUri): void {
86
+        if (!$this->isGloballyEnabled()) {
87
+            return;
88
+        }
89
+
90
+        $targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
91
+        $book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
92
+        $targetPrincipals[] = $book['principaluri'];
93
+        foreach ($targetPrincipals as $principalUri) {
94
+            if (!$this->isUserEnabled($principalUri)) {
95
+                continue;
96
+            }
97
+
98
+            $calendar = $this->ensureCalendarExists($principalUri);
99
+            foreach (['', '-death', '-anniversary'] as $tag) {
100
+                $objectUri = $book['uri'] . '-' . $cardUri . $tag . '.ics';
101
+                $this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
102
+            }
103
+        }
104
+    }
105
+
106
+    /**
107
+     * @throws \Sabre\DAV\Exception\BadRequest
108
+     */
109
+    public function ensureCalendarExists(string $principal): ?array {
110
+        $calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
111
+        if (!is_null($calendar)) {
112
+            return $calendar;
113
+        }
114
+        $this->calDavBackEnd->createCalendar($principal, self::BIRTHDAY_CALENDAR_URI, [
115
+            '{DAV:}displayname' => $this->l10n->t('Contact birthdays'),
116
+            '{http://apple.com/ns/ical/}calendar-color' => '#E9D859',
117
+            'components' => 'VEVENT',
118
+        ]);
119
+
120
+        return $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
121
+    }
122
+
123
+    /**
124
+     * @param $cardData
125
+     * @param $dateField
126
+     * @param $postfix
127
+     * @param $reminderOffset
128
+     * @return VCalendar|null
129
+     * @throws InvalidDataException
130
+     */
131
+    public function buildDateFromContact(string $cardData,
132
+        string $dateField,
133
+        string $postfix,
134
+        ?string $reminderOffset):?VCalendar {
135
+        if (empty($cardData)) {
136
+            return null;
137
+        }
138
+        try {
139
+            $doc = Reader::read($cardData);
140
+            // We're always converting to vCard 4.0 so we can rely on the
141
+            // VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
142
+            if (!$doc instanceof VCard) {
143
+                return null;
144
+            }
145
+            $doc = $doc->convert(Document::VCARD40);
146
+        } catch (Exception $e) {
147
+            return null;
148
+        }
149
+
150
+        if (isset($doc->{self::EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME})) {
151
+            return null;
152
+        }
153
+
154
+        if (!isset($doc->{$dateField})) {
155
+            return null;
156
+        }
157
+        if (!isset($doc->FN)) {
158
+            return null;
159
+        }
160
+        $birthday = $doc->{$dateField};
161
+        if (!(string)$birthday) {
162
+            return null;
163
+        }
164
+        // Skip if the BDAY property is not of the right type.
165
+        if (!$birthday instanceof DateAndOrTime) {
166
+            return null;
167
+        }
168
+
169
+        // Skip if we can't parse the BDAY value.
170
+        try {
171
+            $dateParts = DateTimeParser::parseVCardDateTime($birthday->getValue());
172
+        } catch (InvalidDataException $e) {
173
+            return null;
174
+        }
175
+        if ($dateParts['year'] !== null) {
176
+            $parameters = $birthday->parameters();
177
+            $omitYear = (isset($parameters['X-APPLE-OMIT-YEAR'])
178
+                    && $parameters['X-APPLE-OMIT-YEAR'] === $dateParts['year']);
179
+            // '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
180
+            if ($omitYear || (int)$dateParts['year'] === 1604) {
181
+                $dateParts['year'] = null;
182
+            }
183
+        }
184
+
185
+        $originalYear = null;
186
+        if ($dateParts['year'] !== null) {
187
+            $originalYear = (int)$dateParts['year'];
188
+        }
189
+
190
+        $leapDay = ((int)$dateParts['month'] === 2 && (int)$dateParts['date'] === 29);
191
+
192
+        if ($dateParts['year'] === null) {
193
+            $birthday = ($leapDay ? '1972-' : '1970-')
194
+                . $dateParts['month'] . '-' . $dateParts['date'];
195
+        }
196
+
197
+        try {
198
+            if ($birthday instanceof DateAndOrTime) {
199
+                $date = $birthday->getDateTime();
200
+            } else {
201
+                $date = new \DateTimeImmutable($birthday);
202
+            }
203
+        } catch (Exception $e) {
204
+            return null;
205
+        }
206
+
207
+        $summary = $this->formatTitle($dateField, $doc->FN->getValue(), $originalYear, $this->dbConnection->supports4ByteText());
208
+
209
+        $vCal = new VCalendar();
210
+        $vCal->VERSION = '2.0';
211
+        $vCal->PRODID = '-//IDN nextcloud.com//Birthday calendar//EN';
212
+        $vEvent = $vCal->createComponent('VEVENT');
213
+        $vEvent->add('DTSTART');
214
+        $vEvent->DTSTART->setDateTime(
215
+            $date
216
+        );
217
+        $vEvent->DTSTART['VALUE'] = 'DATE';
218
+        $vEvent->add('DTEND');
219
+
220
+        $dtEndDate = (new \DateTime())->setTimestamp($date->getTimeStamp());
221
+        $dtEndDate->add(new \DateInterval('P1D'));
222
+        $vEvent->DTEND->setDateTime(
223
+            $dtEndDate
224
+        );
225
+
226
+        $vEvent->DTEND['VALUE'] = 'DATE';
227
+        $vEvent->{'UID'} = $doc->UID . $postfix;
228
+        $vEvent->{'RRULE'} = 'FREQ=YEARLY';
229
+        if ($leapDay) {
230
+            /* Sabre\VObject supports BYMONTHDAY only if BYMONTH
231 231
 			 * is also set */
232
-			$vEvent->{'RRULE'} = 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1';
233
-		}
234
-		$vEvent->{'SUMMARY'} = $summary;
235
-		$vEvent->{'TRANSP'} = 'TRANSPARENT';
236
-		$vEvent->{'X-NEXTCLOUD-BC-FIELD-TYPE'} = $dateField;
237
-		$vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $dateParts['year'] === null ? '1' : '0';
238
-		if ($originalYear !== null) {
239
-			$vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string)$originalYear;
240
-		}
241
-		if ($reminderOffset) {
242
-			$alarm = $vCal->createComponent('VALARM');
243
-			$alarm->add($vCal->createProperty('TRIGGER', $reminderOffset, ['VALUE' => 'DURATION']));
244
-			$alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
245
-			$alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
246
-			$vEvent->add($alarm);
247
-		}
248
-		$vCal->add($vEvent);
249
-		return $vCal;
250
-	}
251
-
252
-	/**
253
-	 * @param string $user
254
-	 */
255
-	public function resetForUser(string $user):void {
256
-		$principal = 'principals/users/' . $user;
257
-		$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
258
-		if (!$calendar) {
259
-			return; // The user's birthday calendar doesn't exist, no need to purge it
260
-		}
261
-		$calendarObjects = $this->calDavBackEnd->getCalendarObjects($calendar['id'], CalDavBackend::CALENDAR_TYPE_CALENDAR);
262
-
263
-		foreach ($calendarObjects as $calendarObject) {
264
-			$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $calendarObject['uri'], CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
265
-		}
266
-	}
267
-
268
-	/**
269
-	 * @param string $user
270
-	 * @throws \Sabre\DAV\Exception\BadRequest
271
-	 */
272
-	public function syncUser(string $user):void {
273
-		$principal = 'principals/users/' . $user;
274
-		$this->ensureCalendarExists($principal);
275
-		$books = $this->cardDavBackEnd->getAddressBooksForUser($principal);
276
-		foreach ($books as $book) {
277
-			$cards = $this->cardDavBackEnd->getCards($book['id']);
278
-			foreach ($cards as $card) {
279
-				$this->onCardChanged((int)$book['id'], $card['uri'], $card['carddata']);
280
-			}
281
-		}
282
-	}
283
-
284
-	/**
285
-	 * @param string $existingCalendarData
286
-	 * @param VCalendar $newCalendarData
287
-	 * @return bool
288
-	 */
289
-	public function birthdayEvenChanged(string $existingCalendarData,
290
-		VCalendar $newCalendarData):bool {
291
-		try {
292
-			$existingBirthday = Reader::read($existingCalendarData);
293
-		} catch (Exception $ex) {
294
-			return true;
295
-		}
296
-
297
-		return (
298
-			$newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue()
299
-			|| $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
300
-		);
301
-	}
302
-
303
-	/**
304
-	 * @param integer $addressBookId
305
-	 * @return mixed
306
-	 */
307
-	protected function getAllAffectedPrincipals(int $addressBookId) {
308
-		$targetPrincipals = [];
309
-		$shares = $this->cardDavBackEnd->getShares($addressBookId);
310
-		foreach ($shares as $share) {
311
-			if ($share['{http://owncloud.org/ns}group-share']) {
312
-				$users = $this->principalBackend->getGroupMemberSet($share['{http://owncloud.org/ns}principal']);
313
-				foreach ($users as $user) {
314
-					$targetPrincipals[] = $user['uri'];
315
-				}
316
-			} else {
317
-				$targetPrincipals[] = $share['{http://owncloud.org/ns}principal'];
318
-			}
319
-		}
320
-		return array_values(array_unique($targetPrincipals, SORT_STRING));
321
-	}
322
-
323
-	/**
324
-	 * @param string $cardUri
325
-	 * @param string $cardData
326
-	 * @param array $book
327
-	 * @param int $calendarId
328
-	 * @param array $type
329
-	 * @param string $reminderOffset
330
-	 * @throws InvalidDataException
331
-	 * @throws \Sabre\DAV\Exception\BadRequest
332
-	 */
333
-	private function updateCalendar(string $cardUri,
334
-		string $cardData,
335
-		array $book,
336
-		int $calendarId,
337
-		array $type,
338
-		?string $reminderOffset):void {
339
-		$objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics';
340
-		$calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix'], $reminderOffset);
341
-		$existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri);
342
-		if ($calendarData === null) {
343
-			if ($existing !== null) {
344
-				$this->calDavBackEnd->deleteCalendarObject($calendarId, $objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
345
-			}
346
-		} else {
347
-			if ($existing === null) {
348
-				// not found by URI, but maybe by UID
349
-				// happens when a contact with birthday is moved to a different address book
350
-				$calendarInfo = $this->calDavBackEnd->getCalendarById($calendarId);
351
-				$extraData = $this->calDavBackEnd->getDenormalizedData($calendarData->serialize());
352
-
353
-				if ($calendarInfo && array_key_exists('principaluri', $calendarInfo)) {
354
-					$existing2path = $this->calDavBackEnd->getCalendarObjectByUID($calendarInfo['principaluri'], $extraData['uid']);
355
-					if ($existing2path !== null && array_key_exists('uri', $calendarInfo)) {
356
-						// delete the old birthday entry first so that we do not get duplicate UIDs
357
-						$existing2objectUri = substr($existing2path, strlen($calendarInfo['uri']) + 1);
358
-						$this->calDavBackEnd->deleteCalendarObject($calendarId, $existing2objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
359
-					}
360
-				}
361
-
362
-				$this->calDavBackEnd->createCalendarObject($calendarId, $objectUri, $calendarData->serialize());
363
-			} else {
364
-				if ($this->birthdayEvenChanged($existing['calendardata'], $calendarData)) {
365
-					$this->calDavBackEnd->updateCalendarObject($calendarId, $objectUri, $calendarData->serialize());
366
-				}
367
-			}
368
-		}
369
-	}
370
-
371
-	/**
372
-	 * checks if the admin opted-out of birthday calendars
373
-	 *
374
-	 * @return bool
375
-	 */
376
-	private function isGloballyEnabled():bool {
377
-		return $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes') === 'yes';
378
-	}
379
-
380
-	/**
381
-	 * Extracts the userId part of a principal
382
-	 *
383
-	 * @param string $userPrincipal
384
-	 * @return string|null
385
-	 */
386
-	private function principalToUserId(string $userPrincipal):?string {
387
-		if (str_starts_with($userPrincipal, 'principals/users/')) {
388
-			return substr($userPrincipal, 17);
389
-		}
390
-		return null;
391
-	}
392
-
393
-	/**
394
-	 * Checks if the user opted-out of birthday calendars
395
-	 *
396
-	 * @param string $userPrincipal The user principal to check for
397
-	 * @return bool
398
-	 */
399
-	private function isUserEnabled(string $userPrincipal):bool {
400
-		$userId = $this->principalToUserId($userPrincipal);
401
-		if ($userId !== null) {
402
-			$isEnabled = $this->config->getUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes');
403
-			return $isEnabled === 'yes';
404
-		}
405
-
406
-		// not sure how we got here, just be on the safe side and return true
407
-		return true;
408
-	}
409
-
410
-	/**
411
-	 * Get the reminder offset value for a user. This is a duration string (e.g.
412
-	 * PT9H) or null if no reminder is wanted.
413
-	 *
414
-	 * @param string $userPrincipal
415
-	 * @return string|null
416
-	 */
417
-	private function getReminderOffsetForUser(string $userPrincipal):?string {
418
-		$userId = $this->principalToUserId($userPrincipal);
419
-		if ($userId !== null) {
420
-			return $this->config->getUserValue($userId, 'dav', 'birthdayCalendarReminderOffset', 'PT9H') ?: null;
421
-		}
422
-
423
-		// not sure how we got here, just be on the safe side and return the default value
424
-		return 'PT9H';
425
-	}
426
-
427
-	/**
428
-	 * Formats title of Birthday event
429
-	 *
430
-	 * @param string $field Field name like BDAY, ANNIVERSARY, ...
431
-	 * @param string $name Name of contact
432
-	 * @param int|null $year Year of birth, anniversary, ...
433
-	 * @param bool $supports4Byte Whether or not the database supports 4 byte chars
434
-	 * @return string The formatted title
435
-	 */
436
-	private function formatTitle(string $field,
437
-		string $name,
438
-		?int $year = null,
439
-		bool $supports4Byte = true):string {
440
-		if ($supports4Byte) {
441
-			switch ($field) {
442
-				case 'BDAY':
443
-					return implode('', [
444
-						'
Please login to merge, or discard this patch.
Spacing   +22 added lines, -22 removed lines patch added patch discarded remove patch
@@ -76,7 +76,7 @@  discard block
 block discarded – undo
76 76
 				return;
77 77
 			}
78 78
 			foreach ($datesToSync as $type) {
79
-				$this->updateCalendar($cardUri, $cardData, $book, (int)$calendar['id'], $type, $reminderOffset);
79
+				$this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type, $reminderOffset);
80 80
 			}
81 81
 		}
82 82
 	}
@@ -97,7 +97,7 @@  discard block
 block discarded – undo
97 97
 
98 98
 			$calendar = $this->ensureCalendarExists($principalUri);
99 99
 			foreach (['', '-death', '-anniversary'] as $tag) {
100
-				$objectUri = $book['uri'] . '-' . $cardUri . $tag . '.ics';
100
+				$objectUri = $book['uri'].'-'.$cardUri.$tag.'.ics';
101 101
 				$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
102 102
 			}
103 103
 		}
@@ -131,7 +131,7 @@  discard block
 block discarded – undo
131 131
 	public function buildDateFromContact(string $cardData,
132 132
 		string $dateField,
133 133
 		string $postfix,
134
-		?string $reminderOffset):?VCalendar {
134
+		?string $reminderOffset): ?VCalendar {
135 135
 		if (empty($cardData)) {
136 136
 			return null;
137 137
 		}
@@ -158,7 +158,7 @@  discard block
 block discarded – undo
158 158
 			return null;
159 159
 		}
160 160
 		$birthday = $doc->{$dateField};
161
-		if (!(string)$birthday) {
161
+		if (!(string) $birthday) {
162 162
 			return null;
163 163
 		}
164 164
 		// Skip if the BDAY property is not of the right type.
@@ -177,21 +177,21 @@  discard block
 block discarded – undo
177 177
 			$omitYear = (isset($parameters['X-APPLE-OMIT-YEAR'])
178 178
 					&& $parameters['X-APPLE-OMIT-YEAR'] === $dateParts['year']);
179 179
 			// '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
180
-			if ($omitYear || (int)$dateParts['year'] === 1604) {
180
+			if ($omitYear || (int) $dateParts['year'] === 1604) {
181 181
 				$dateParts['year'] = null;
182 182
 			}
183 183
 		}
184 184
 
185 185
 		$originalYear = null;
186 186
 		if ($dateParts['year'] !== null) {
187
-			$originalYear = (int)$dateParts['year'];
187
+			$originalYear = (int) $dateParts['year'];
188 188
 		}
189 189
 
190
-		$leapDay = ((int)$dateParts['month'] === 2 && (int)$dateParts['date'] === 29);
190
+		$leapDay = ((int) $dateParts['month'] === 2 && (int) $dateParts['date'] === 29);
191 191
 
192 192
 		if ($dateParts['year'] === null) {
193 193
 			$birthday = ($leapDay ? '1972-' : '1970-')
194
-				. $dateParts['month'] . '-' . $dateParts['date'];
194
+				. $dateParts['month'].'-'.$dateParts['date'];
195 195
 		}
196 196
 
197 197
 		try {
@@ -224,7 +224,7 @@  discard block
 block discarded – undo
224 224
 		);
225 225
 
226 226
 		$vEvent->DTEND['VALUE'] = 'DATE';
227
-		$vEvent->{'UID'} = $doc->UID . $postfix;
227
+		$vEvent->{'UID'} = $doc->UID.$postfix;
228 228
 		$vEvent->{'RRULE'} = 'FREQ=YEARLY';
229 229
 		if ($leapDay) {
230 230
 			/* Sabre\VObject supports BYMONTHDAY only if BYMONTH
@@ -236,7 +236,7 @@  discard block
 block discarded – undo
236 236
 		$vEvent->{'X-NEXTCLOUD-BC-FIELD-TYPE'} = $dateField;
237 237
 		$vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $dateParts['year'] === null ? '1' : '0';
238 238
 		if ($originalYear !== null) {
239
-			$vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string)$originalYear;
239
+			$vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string) $originalYear;
240 240
 		}
241 241
 		if ($reminderOffset) {
242 242
 			$alarm = $vCal->createComponent('VALARM');
@@ -253,7 +253,7 @@  discard block
 block discarded – undo
253 253
 	 * @param string $user
254 254
 	 */
255 255
 	public function resetForUser(string $user):void {
256
-		$principal = 'principals/users/' . $user;
256
+		$principal = 'principals/users/'.$user;
257 257
 		$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
258 258
 		if (!$calendar) {
259 259
 			return; // The user's birthday calendar doesn't exist, no need to purge it
@@ -270,13 +270,13 @@  discard block
 block discarded – undo
270 270
 	 * @throws \Sabre\DAV\Exception\BadRequest
271 271
 	 */
272 272
 	public function syncUser(string $user):void {
273
-		$principal = 'principals/users/' . $user;
273
+		$principal = 'principals/users/'.$user;
274 274
 		$this->ensureCalendarExists($principal);
275 275
 		$books = $this->cardDavBackEnd->getAddressBooksForUser($principal);
276 276
 		foreach ($books as $book) {
277 277
 			$cards = $this->cardDavBackEnd->getCards($book['id']);
278 278
 			foreach ($cards as $card) {
279
-				$this->onCardChanged((int)$book['id'], $card['uri'], $card['carddata']);
279
+				$this->onCardChanged((int) $book['id'], $card['uri'], $card['carddata']);
280 280
 			}
281 281
 		}
282 282
 	}
@@ -336,7 +336,7 @@  discard block
 block discarded – undo
336 336
 		int $calendarId,
337 337
 		array $type,
338 338
 		?string $reminderOffset):void {
339
-		$objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics';
339
+		$objectUri = $book['uri'].'-'.$cardUri.$type['postfix'].'.ics';
340 340
 		$calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix'], $reminderOffset);
341 341
 		$existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri);
342 342
 		if ($calendarData === null) {
@@ -383,7 +383,7 @@  discard block
 block discarded – undo
383 383
 	 * @param string $userPrincipal
384 384
 	 * @return string|null
385 385
 	 */
386
-	private function principalToUserId(string $userPrincipal):?string {
386
+	private function principalToUserId(string $userPrincipal): ?string {
387 387
 		if (str_starts_with($userPrincipal, 'principals/users/')) {
388 388
 			return substr($userPrincipal, 17);
389 389
 		}
@@ -414,7 +414,7 @@  discard block
 block discarded – undo
414 414
 	 * @param string $userPrincipal
415 415
 	 * @return string|null
416 416
 	 */
417
-	private function getReminderOffsetForUser(string $userPrincipal):?string {
417
+	private function getReminderOffsetForUser(string $userPrincipal): ?string {
418 418
 		$userId = $this->principalToUserId($userPrincipal);
419 419
 		if ($userId !== null) {
420 420
 			return $this->config->getUserValue($userId, 'dav', 'birthdayCalendarReminderOffset', 'PT9H') ?: null;
@@ -443,20 +443,20 @@  discard block
 block discarded – undo
443 443
 					return implode('', [
444 444
 						'
Please login to merge, or discard this patch.
apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php 1 patch
Indentation   +409 added lines, -409 removed lines patch added patch discarded remove patch
@@ -21,413 +21,413 @@
 block discarded – undo
21 21
 use Test\TestCase;
22 22
 
23 23
 class BirthdayServiceTest extends TestCase {
24
-	private CalDavBackend&MockObject $calDav;
25
-	private CardDavBackend&MockObject $cardDav;
26
-	private GroupPrincipalBackend&MockObject $groupPrincipalBackend;
27
-	private IConfig&MockObject $config;
28
-	private IDBConnection&MockObject $dbConnection;
29
-	private IL10N&MockObject $l10n;
30
-	private BirthdayService $service;
31
-
32
-	protected function setUp(): void {
33
-		parent::setUp();
34
-
35
-		$this->calDav = $this->createMock(CalDavBackend::class);
36
-		$this->cardDav = $this->createMock(CardDavBackend::class);
37
-		$this->groupPrincipalBackend = $this->createMock(GroupPrincipalBackend::class);
38
-		$this->config = $this->createMock(IConfig::class);
39
-		$this->dbConnection = $this->createMock(IDBConnection::class);
40
-		$this->l10n = $this->createMock(IL10N::class);
41
-
42
-		$this->l10n->expects($this->any())
43
-			->method('t')
44
-			->willReturnCallback(function ($string, $args) {
45
-				return vsprintf($string, $args);
46
-			});
47
-
48
-		$this->service = new BirthdayService($this->calDav, $this->cardDav,
49
-			$this->groupPrincipalBackend, $this->config,
50
-			$this->dbConnection, $this->l10n);
51
-	}
52
-
53
-	#[\PHPUnit\Framework\Attributes\DataProvider('providesVCards')]
54
-	public function testBuildBirthdayFromContact(?string $expectedSummary, ?string $expectedDTStart, ?string $expectedRrule, ?string $expectedFieldType, ?string $expectedUnknownYear, ?string $expectedOriginalYear, ?string $expectedReminder, ?string $data, string $fieldType, string $prefix, bool $supports4Bytes, ?string $configuredReminder): void {
55
-		$this->dbConnection->method('supports4ByteText')->willReturn($supports4Bytes);
56
-		$cal = $this->service->buildDateFromContact($data, $fieldType, $prefix, $configuredReminder);
57
-
58
-		if ($expectedSummary === null) {
59
-			$this->assertNull($cal);
60
-		} else {
61
-			$this->assertInstanceOf('Sabre\VObject\Component\VCalendar', $cal);
62
-			$this->assertEquals('-//IDN nextcloud.com//Birthday calendar//EN', $cal->PRODID->getValue());
63
-			$this->assertTrue(isset($cal->VEVENT));
64
-			$this->assertEquals($expectedRrule, $cal->VEVENT->RRULE->getValue());
65
-			$this->assertEquals($expectedSummary, $cal->VEVENT->SUMMARY->getValue());
66
-			$this->assertEquals($expectedDTStart, $cal->VEVENT->DTSTART->getValue());
67
-			$this->assertEquals($expectedFieldType, $cal->VEVENT->{'X-NEXTCLOUD-BC-FIELD-TYPE'}->getValue());
68
-			$this->assertEquals($expectedUnknownYear, $cal->VEVENT->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'}->getValue());
69
-
70
-			if ($expectedOriginalYear) {
71
-				$this->assertEquals($expectedOriginalYear, $cal->VEVENT->{'X-NEXTCLOUD-BC-YEAR'}->getValue());
72
-			}
73
-
74
-			if ($expectedReminder) {
75
-				$this->assertEquals($expectedReminder, $cal->VEVENT->VALARM->TRIGGER->getValue());
76
-				$this->assertEquals('DURATION', $cal->VEVENT->VALARM->TRIGGER->getValueType());
77
-			}
78
-
79
-			$this->assertEquals('TRANSPARENT', $cal->VEVENT->TRANSP->getValue());
80
-		}
81
-	}
82
-
83
-	public function testOnCardDeleteGloballyDisabled(): void {
84
-		$this->config->expects($this->once())
85
-			->method('getAppValue')
86
-			->with('dav', 'generateBirthdayCalendar', 'yes')
87
-			->willReturn('no');
88
-
89
-		$this->cardDav->expects($this->never())->method('getAddressBookById');
90
-
91
-		$this->service->onCardDeleted(666, 'gump.vcf');
92
-	}
93
-
94
-	public function testOnCardDeleteUserDisabled(): void {
95
-		$this->config->expects($this->once())
96
-			->method('getAppValue')
97
-			->with('dav', 'generateBirthdayCalendar', 'yes')
98
-			->willReturn('yes');
99
-
100
-		$this->config->expects($this->once())
101
-			->method('getUserValue')
102
-			->with('user01', 'dav', 'generateBirthdayCalendar', 'yes')
103
-			->willReturn('no');
104
-
105
-		$this->cardDav->expects($this->once())->method('getAddressBookById')
106
-			->with(666)
107
-			->willReturn([
108
-				'principaluri' => 'principals/users/user01',
109
-				'uri' => 'default'
110
-			]);
111
-		$this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
112
-		$this->calDav->expects($this->never())->method('getCalendarByUri');
113
-		$this->calDav->expects($this->never())->method('deleteCalendarObject');
114
-
115
-		$this->service->onCardDeleted(666, 'gump.vcf');
116
-	}
117
-
118
-	public function testOnCardDeleted(): void {
119
-		$this->config->expects($this->once())
120
-			->method('getAppValue')
121
-			->with('dav', 'generateBirthdayCalendar', 'yes')
122
-			->willReturn('yes');
123
-
124
-		$this->config->expects($this->once())
125
-			->method('getUserValue')
126
-			->with('user01', 'dav', 'generateBirthdayCalendar', 'yes')
127
-			->willReturn('yes');
128
-
129
-		$this->cardDav->expects($this->once())->method('getAddressBookById')
130
-			->with(666)
131
-			->willReturn([
132
-				'principaluri' => 'principals/users/user01',
133
-				'uri' => 'default'
134
-			]);
135
-		$this->calDav->expects($this->once())->method('getCalendarByUri')
136
-			->with('principals/users/user01', 'contact_birthdays')
137
-			->willReturn([
138
-				'id' => 1234
139
-			]);
140
-		$calls = [
141
-			[1234, 'default-gump.vcf.ics'],
142
-			[1234, 'default-gump.vcf-death.ics'],
143
-			[1234, 'default-gump.vcf-anniversary.ics'],
144
-		];
145
-		$this->calDav->expects($this->exactly(count($calls)))
146
-			->method('deleteCalendarObject')
147
-			->willReturnCallback(function ($calendarId, $objectUri) use (&$calls): void {
148
-				$expected = array_shift($calls);
149
-				$this->assertEquals($expected, [$calendarId, $objectUri]);
150
-			});
151
-		$this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
152
-
153
-		$this->service->onCardDeleted(666, 'gump.vcf');
154
-	}
155
-
156
-	public function testOnCardChangedGloballyDisabled(): void {
157
-		$this->config->expects($this->once())
158
-			->method('getAppValue')
159
-			->with('dav', 'generateBirthdayCalendar', 'yes')
160
-			->willReturn('no');
161
-
162
-		$this->cardDav->expects($this->never())->method('getAddressBookById');
163
-
164
-		$service = $this->getMockBuilder(BirthdayService::class)
165
-			->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged'])
166
-			->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n])
167
-			->getMock();
168
-
169
-		$service->onCardChanged(666, 'gump.vcf', '');
170
-	}
171
-
172
-	public function testOnCardChangedUserDisabled(): void {
173
-		$this->config->expects($this->once())
174
-			->method('getAppValue')
175
-			->with('dav', 'generateBirthdayCalendar', 'yes')
176
-			->willReturn('yes');
177
-
178
-		$this->config->expects($this->once())
179
-			->method('getUserValue')
180
-			->with('user01', 'dav', 'generateBirthdayCalendar', 'yes')
181
-			->willReturn('no');
182
-
183
-		$this->cardDav->expects($this->once())->method('getAddressBookById')
184
-			->with(666)
185
-			->willReturn([
186
-				'principaluri' => 'principals/users/user01',
187
-				'uri' => 'default'
188
-			]);
189
-		$this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
190
-		$this->calDav->expects($this->never())->method('getCalendarByUri');
191
-
192
-		/** @var BirthdayService&MockObject $service */
193
-		$service = $this->getMockBuilder(BirthdayService::class)
194
-			->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged'])
195
-			->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n])
196
-			->getMock();
197
-
198
-		$service->onCardChanged(666, 'gump.vcf', '');
199
-	}
200
-
201
-	#[\PHPUnit\Framework\Attributes\DataProvider('providesCardChanges')]
202
-	public function testOnCardChanged(string $expectedOp): void {
203
-		$this->config->expects($this->once())
204
-			->method('getAppValue')
205
-			->with('dav', 'generateBirthdayCalendar', 'yes')
206
-			->willReturn('yes');
207
-
208
-		$this->config->expects($this->exactly(2))
209
-			->method('getUserValue')
210
-			->willReturnMap([
211
-				['user01', 'dav', 'generateBirthdayCalendar', 'yes', 'yes'],
212
-				['user01', 'dav', 'birthdayCalendarReminderOffset', 'PT9H', 'PT9H'],
213
-			]);
214
-
215
-		$this->cardDav->expects($this->once())->method('getAddressBookById')
216
-			->with(666)
217
-			->willReturn([
218
-				'principaluri' => 'principals/users/user01',
219
-				'uri' => 'default'
220
-			]);
221
-		$this->calDav->expects($this->once())->method('getCalendarByUri')
222
-			->with('principals/users/user01', 'contact_birthdays')
223
-			->willReturn([
224
-				'id' => 1234
225
-			]);
226
-		$this->cardDav->expects($this->once())->method('getShares')->willReturn([]);
227
-
228
-		/** @var BirthdayService&MockObject $service */
229
-		$service = $this->getMockBuilder(BirthdayService::class)
230
-			->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged'])
231
-			->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n])
232
-			->getMock();
233
-
234
-		if ($expectedOp === 'delete') {
235
-			$this->calDav->expects($this->exactly(3))->method('getCalendarObject')->willReturn('');
236
-			$service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn(null);
237
-
238
-			$calls = [
239
-				[1234, 'default-gump.vcf.ics'],
240
-				[1234, 'default-gump.vcf-death.ics'],
241
-				[1234, 'default-gump.vcf-anniversary.ics']
242
-			];
243
-			$this->calDav->expects($this->exactly(count($calls)))
244
-				->method('deleteCalendarObject')
245
-				->willReturnCallback(function ($calendarId, $objectUri) use (&$calls): void {
246
-					$expected = array_shift($calls);
247
-					$this->assertEquals($expected, [$calendarId, $objectUri]);
248
-				});
249
-		}
250
-		if ($expectedOp === 'create') {
251
-			$vCal = new VCalendar();
252
-			$vCal->PRODID = '-//Nextcloud testing//mocked object//';
253
-
254
-			$service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn($vCal);
255
-
256
-			$createCalendarObjectCalls = [
257
-				[1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
258
-				[1234, 'default-gump.vcf-death.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
259
-				[1234, 'default-gump.vcf-anniversary.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"]
260
-			];
261
-			$this->calDav->expects($this->exactly(count($createCalendarObjectCalls)))
262
-				->method('createCalendarObject')
263
-				->willReturnCallback(function ($calendarId, $objectUri, $calendarData) use (&$createCalendarObjectCalls): void {
264
-					$expected = array_shift($createCalendarObjectCalls);
265
-					$this->assertEquals($expected, [$calendarId, $objectUri, $calendarData]);
266
-				});
267
-		}
268
-		if ($expectedOp === 'update') {
269
-			$vCal = new VCalendar();
270
-			$vCal->PRODID = '-//Nextcloud testing//mocked object//';
271
-
272
-			$service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn($vCal);
273
-			$service->expects($this->exactly(3))->method('birthdayEvenChanged')->willReturn(true);
274
-			$this->calDav->expects($this->exactly(3))->method('getCalendarObject')->willReturn(['calendardata' => '']);
275
-
276
-			$updateCalendarObjectCalls = [
277
-				[1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
278
-				[1234, 'default-gump.vcf-death.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"],
279
-				[1234, 'default-gump.vcf-anniversary.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"]
280
-			];
281
-			$this->calDav->expects($this->exactly(count($updateCalendarObjectCalls)))
282
-				->method('updateCalendarObject')
283
-				->willReturnCallback(function ($calendarId, $objectUri, $calendarData) use (&$updateCalendarObjectCalls): void {
284
-					$expected = array_shift($updateCalendarObjectCalls);
285
-					$this->assertEquals($expected, [$calendarId, $objectUri, $calendarData]);
286
-				});
287
-		}
288
-
289
-		$service->onCardChanged(666, 'gump.vcf', '');
290
-	}
291
-
292
-	#[\PHPUnit\Framework\Attributes\DataProvider('providesBirthday')]
293
-	public function testBirthdayEvenChanged(bool $expected, string $old, string $new): void {
294
-		$new = Reader::read($new);
295
-		$this->assertEquals($expected, $this->service->birthdayEvenChanged($old, $new));
296
-	}
297
-
298
-	public function testGetAllAffectedPrincipals(): void {
299
-		$this->cardDav->expects($this->once())->method('getShares')->willReturn([
300
-			[
301
-				'{http://owncloud.org/ns}group-share' => false,
302
-				'{http://owncloud.org/ns}principal' => 'principals/users/user01'
303
-			],
304
-			[
305
-				'{http://owncloud.org/ns}group-share' => false,
306
-				'{http://owncloud.org/ns}principal' => 'principals/users/user01'
307
-			],
308
-			[
309
-				'{http://owncloud.org/ns}group-share' => false,
310
-				'{http://owncloud.org/ns}principal' => 'principals/users/user02'
311
-			],
312
-			[
313
-				'{http://owncloud.org/ns}group-share' => true,
314
-				'{http://owncloud.org/ns}principal' => 'principals/groups/users'
315
-			],
316
-		]);
317
-		$this->groupPrincipalBackend->expects($this->once())->method('getGroupMemberSet')
318
-			->willReturn([
319
-				[
320
-					'uri' => 'principals/users/user01',
321
-				],
322
-				[
323
-					'uri' => 'principals/users/user02',
324
-				],
325
-				[
326
-					'uri' => 'principals/users/user03',
327
-				],
328
-			]);
329
-		$users = $this->invokePrivate($this->service, 'getAllAffectedPrincipals', [6666]);
330
-		$this->assertEquals([
331
-			'principals/users/user01',
332
-			'principals/users/user02',
333
-			'principals/users/user03'
334
-		], $users);
335
-	}
336
-
337
-	public function testBirthdayCalendarHasComponentEvent(): void {
338
-		$this->calDav->expects($this->once())
339
-			->method('createCalendar')
340
-			->with('principal001', 'contact_birthdays', [
341
-				'{DAV:}displayname' => 'Contact birthdays',
342
-				'{http://apple.com/ns/ical/}calendar-color' => '#E9D859',
343
-				'components' => 'VEVENT',
344
-			]);
345
-		$this->service->ensureCalendarExists('principal001');
346
-	}
347
-
348
-	public function testResetForUser(): void {
349
-		$this->calDav->expects($this->once())
350
-			->method('getCalendarByUri')
351
-			->with('principals/users/user123', 'contact_birthdays')
352
-			->willReturn(['id' => 42]);
353
-
354
-		$this->calDav->expects($this->once())
355
-			->method('getCalendarObjects')
356
-			->with(42, 0)
357
-			->willReturn([['uri' => '1.ics'], ['uri' => '2.ics'], ['uri' => '3.ics']]);
358
-
359
-		$calls = [
360
-			[42, '1.ics', 0],
361
-			[42, '2.ics', 0],
362
-			[42, '3.ics', 0],
363
-		];
364
-		$this->calDav->expects($this->exactly(count($calls)))
365
-			->method('deleteCalendarObject')
366
-			->willReturnCallback(function ($calendarId, $objectUri, $calendarType) use (&$calls): void {
367
-				$expected = array_shift($calls);
368
-				$this->assertEquals($expected, [$calendarId, $objectUri, $calendarType]);
369
-			});
370
-
371
-		$this->service->resetForUser('user123');
372
-	}
373
-
374
-	public static function providesBirthday(): array {
375
-		return [
376
-			[true,
377
-				'',
378
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
379
-			[false,
380
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
381
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
382
-			[true,
383
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:4567's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
384
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"],
385
-			[true,
386
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n",
387
-				"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000102\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"]
388
-		];
389
-	}
390
-
391
-	public static function providesCardChanges(): array {
392
-		return[
393
-			['delete'],
394
-			['create'],
395
-			['update']
396
-		];
397
-	}
398
-
399
-	public static function providesVCards(): array {
400
-		return [
401
-			// $expectedSummary, $expectedDTStart, $expectedRrule, $expectedFieldType, $expectedUnknownYear, $expectedOriginalYear, $expectedReminder, $data, $fieldType, $prefix, $supports4Byte, $configuredReminder
402
-			[null, null, null, null, null, null, null, 'yasfewf', '', '', true, null],
403
-			[null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
404
-			[null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
405
-			[null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:someday\r\nEND:VCARD\r\n", 'BDAY', '', true, null],
406
-			['
Please login to merge, or discard this patch.