Passed
Push — master ( 920174...784b2b )
by Blizzz
16:18 queued 14s
created
apps/dav/lib/CalDAV/Schedule/IMipService.php 2 patches
Indentation   +563 added lines, -563 removed lines patch added patch discarded remove patch
@@ -40,567 +40,567 @@
 block discarded – undo
40 40
 
41 41
 class IMipService {
42 42
 
43
-	private URLGenerator $urlGenerator;
44
-	private IConfig $config;
45
-	private IDBConnection $db;
46
-	private ISecureRandom $random;
47
-	private L10NFactory $l10nFactory;
48
-	private IL10N $l10n;
49
-
50
-	/** @var string[] */
51
-	private const STRING_DIFF = [
52
-		'meeting_title' => 'SUMMARY',
53
-		'meeting_description' => 'DESCRIPTION',
54
-		'meeting_url' => 'URL',
55
-		'meeting_location' => 'LOCATION'
56
-	];
57
-
58
-	public function __construct(URLGenerator $urlGenerator,
59
-								IConfig $config,
60
-								IDBConnection $db,
61
-								ISecureRandom $random,
62
-								L10NFactory $l10nFactory) {
63
-		$this->urlGenerator = $urlGenerator;
64
-		$this->config = $config;
65
-		$this->db = $db;
66
-		$this->random = $random;
67
-		$this->l10nFactory = $l10nFactory;
68
-		$default = $this->l10nFactory->findGenericLanguage();
69
-		$this->l10n = $this->l10nFactory->get('dav', $default);
70
-	}
71
-
72
-	/**
73
-	 * @param string|null $senderName
74
-	 * @param string $default
75
-	 * @return string
76
-	 */
77
-	public function getFrom(?string $senderName, string $default): string {
78
-		if ($senderName === null) {
79
-			return $default;
80
-		}
81
-
82
-		return $this->l10n->t('%1$s via %2$s', [$senderName, $default]);
83
-	}
84
-
85
-	public static function readPropertyWithDefault(VEvent $vevent, string $property, string $default) {
86
-		if (isset($vevent->$property)) {
87
-			$value = $vevent->$property->getValue();
88
-			if (!empty($value)) {
89
-				return $value;
90
-			}
91
-		}
92
-		return $default;
93
-	}
94
-
95
-	private function generateDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
96
-		$strikethrough = "<span style='text-decoration: line-through'>%s</span><br />%s";
97
-		if (!isset($vevent->$property)) {
98
-			return $default;
99
-		}
100
-		$newstring = $vevent->$property->getValue();
101
-		if(isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring ) {
102
-			$oldstring = $oldVEvent->$property->getValue();
103
-			return sprintf($strikethrough, $oldstring, $newstring);
104
-		}
105
-		return $newstring;
106
-	}
107
-
108
-	/**
109
-	 * @param VEvent $vEvent
110
-	 * @param VEvent|null $oldVEvent
111
-	 * @return array
112
-	 */
113
-	public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array {
114
-		$defaultVal = '';
115
-		$data = [];
116
-		$data['meeting_when'] = $this->generateWhenString($vEvent);
117
-
118
-		foreach(self::STRING_DIFF as $key => $property) {
119
-			$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
120
-		}
121
-
122
-		$data['meeting_url_html'] = self::readPropertyWithDefault($vEvent, 'URL', $defaultVal);
123
-
124
-		if(!empty($oldVEvent)) {
125
-			$oldMeetingWhen = $this->generateWhenString($oldVEvent);
126
-			$data['meeting_title_html']	= $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
127
-			$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
128
-			$data['meeting_location_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
129
-
130
-			$oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
131
-			$data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url'];
132
-
133
-			$data['meeting_when_html'] =
134
-				($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null)
135
-					? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when'])
136
-					: $data['meeting_when'];
137
-		}
138
-		return $data;
139
-	}
140
-
141
-	/**
142
-	 * @param IL10N $this->l10n
143
-	 * @param VEvent $vevent
144
-	 * @return false|int|string
145
-	 */
146
-	public function generateWhenString(VEvent $vevent) {
147
-		/** @var Property\ICalendar\DateTime $dtstart */
148
-		$dtstart = $vevent->DTSTART;
149
-		if (isset($vevent->DTEND)) {
150
-			/** @var Property\ICalendar\DateTime $dtend */
151
-			$dtend = $vevent->DTEND;
152
-		} elseif (isset($vevent->DURATION)) {
153
-			$isFloating = $dtstart->isFloating();
154
-			$dtend = clone $dtstart;
155
-			$endDateTime = $dtend->getDateTime();
156
-			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
157
-			$dtend->setDateTime($endDateTime, $isFloating);
158
-		} elseif (!$dtstart->hasTime()) {
159
-			$isFloating = $dtstart->isFloating();
160
-			$dtend = clone $dtstart;
161
-			$endDateTime = $dtend->getDateTime();
162
-			$endDateTime = $endDateTime->modify('+1 day');
163
-			$dtend->setDateTime($endDateTime, $isFloating);
164
-		} else {
165
-			$dtend = clone $dtstart;
166
-		}
167
-
168
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
169
-		/** @var \DateTimeImmutable $dtstartDt */
170
-		$dtstartDt = $dtstart->getDateTime();
171
-
172
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
173
-		/** @var \DateTimeImmutable $dtendDt */
174
-		$dtendDt = $dtend->getDateTime();
175
-
176
-		$diff = $dtstartDt->diff($dtendDt);
177
-
178
-		$dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
179
-		$dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
180
-
181
-		if ($dtstart instanceof Property\ICalendar\Date) {
182
-			// One day event
183
-			if ($diff->days === 1) {
184
-				return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
185
-			}
186
-
187
-			// DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
188
-			// the email should show 2020-01-01 to 2020-01-04.
189
-			$dtendDt->modify('-1 day');
190
-
191
-			//event that spans over multiple days
192
-			$localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
193
-			$localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']);
194
-
195
-			return $localeStart . ' - ' . $localeEnd;
196
-		}
197
-
198
-		/** @var Property\ICalendar\DateTime $dtstart */
199
-		/** @var Property\ICalendar\DateTime $dtend */
200
-		$isFloating = $dtstart->isFloating();
201
-		$startTimezone = $endTimezone = null;
202
-		if (!$isFloating) {
203
-			$prop = $dtstart->offsetGet('TZID');
204
-			if ($prop instanceof Parameter) {
205
-				$startTimezone = $prop->getValue();
206
-			}
207
-
208
-			$prop = $dtend->offsetGet('TZID');
209
-			if ($prop instanceof Parameter) {
210
-				$endTimezone = $prop->getValue();
211
-			}
212
-		}
213
-
214
-		$localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
215
-			$this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
216
-
217
-		// always show full date with timezone if timezones are different
218
-		if ($startTimezone !== $endTimezone) {
219
-			$localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
220
-
221
-			return $localeStart . ' (' . $startTimezone . ') - ' .
222
-				$localeEnd . ' (' . $endTimezone . ')';
223
-		}
224
-
225
-		// show only end time if date is the same
226
-		if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) {
227
-			$localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']);
228
-		} else {
229
-			$localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
230
-				$this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
231
-		}
232
-
233
-		return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
234
-	}
235
-
236
-	/**
237
-	 * @param VEvent $vEvent
238
-	 * @return array
239
-	 */
240
-	public function buildCancelledBodyData(VEvent $vEvent): array {
241
-		$defaultVal = '';
242
-		$strikethrough = "<span style='text-decoration: line-through'>%s</span>";
243
-
244
-		$newMeetingWhen = $this->generateWhenString($vEvent);
245
-		$newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');;
246
-		$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
247
-		$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
248
-		$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
249
-
250
-		$data = [];
251
-		$data['meeting_when_html'] = $newMeetingWhen === '' ?: sprintf($strikethrough, $newMeetingWhen);
252
-		$data['meeting_when'] = $newMeetingWhen;
253
-		$data['meeting_title_html'] = sprintf($strikethrough, $newSummary);
254
-		$data['meeting_title'] = $newSummary !== '' ? $newSummary: $this->l10n->t('Untitled event');
255
-		$data['meeting_description_html'] = $newDescription !== '' ? sprintf($strikethrough, $newDescription) : '';
256
-		$data['meeting_description'] = $newDescription;
257
-		$data['meeting_url_html'] = $newUrl !== '' ? sprintf($strikethrough, $newUrl) : '';
258
-		$data['meeting_url'] = isset($vEvent->URL) ? (string)$vEvent->URL : '';
259
-		$data['meeting_location_html'] = $newLocation !== '' ? sprintf($strikethrough, $newLocation) : '';
260
-		$data['meeting_location'] = $newLocation;
261
-		return $data;
262
-	}
263
-
264
-	/**
265
-	 * Check if event took place in the past
266
-	 *
267
-	 * @param VCalendar $vObject
268
-	 * @return int
269
-	 */
270
-	public function getLastOccurrence(VCalendar $vObject) {
271
-		/** @var VEvent $component */
272
-		$component = $vObject->VEVENT;
273
-
274
-		if (isset($component->RRULE)) {
275
-			$it = new EventIterator($vObject, (string)$component->UID);
276
-			$maxDate = new \DateTime(IMipPlugin::MAX_DATE);
277
-			if ($it->isInfinite()) {
278
-				return $maxDate->getTimestamp();
279
-			}
280
-
281
-			$end = $it->getDtEnd();
282
-			while ($it->valid() && $end < $maxDate) {
283
-				$end = $it->getDtEnd();
284
-				$it->next();
285
-			}
286
-			return $end->getTimestamp();
287
-		}
288
-
289
-		/** @var Property\ICalendar\DateTime $dtStart */
290
-		$dtStart = $component->DTSTART;
291
-
292
-		if (isset($component->DTEND)) {
293
-			/** @var Property\ICalendar\DateTime $dtEnd */
294
-			$dtEnd = $component->DTEND;
295
-			return $dtEnd->getDateTime()->getTimeStamp();
296
-		}
297
-
298
-		if(isset($component->DURATION)) {
299
-			/** @var \DateTime $endDate */
300
-			$endDate = clone $dtStart->getDateTime();
301
-			// $component->DTEND->getDateTime() returns DateTimeImmutable
302
-			$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
303
-			return $endDate->getTimestamp();
304
-		}
305
-
306
-		if(!$dtStart->hasTime()) {
307
-			/** @var \DateTime $endDate */
308
-			// $component->DTSTART->getDateTime() returns DateTimeImmutable
309
-			$endDate = clone $dtStart->getDateTime();
310
-			$endDate = $endDate->modify('+1 day');
311
-			return $endDate->getTimestamp();
312
-		}
313
-
314
-		// No computation of end time possible - return start date
315
-		return $dtStart->getDateTime()->getTimeStamp();
316
-	}
317
-
318
-	/**
319
-	 * @param Property|null $attendee
320
-	 */
321
-	public function setL10n(?Property $attendee = null) {
322
-		if($attendee === null) {
323
-			return;
324
-		}
325
-
326
-		$lang = $attendee->offsetGet('LANGUAGE');
327
-		if ($lang instanceof Parameter) {
328
-			$lang = $lang->getValue();
329
-			$this->l10n = $this->l10nFactory->get('dav', $lang);
330
-		}
331
-	}
332
-
333
-	/**
334
-	 * @param Property|null $attendee
335
-	 * @return bool
336
-	 */
337
-	public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) {
338
-		if($attendee === null) {
339
-			return false;
340
-		}
341
-
342
-		$rsvp = $attendee->offsetGet('RSVP');
343
-		if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
344
-			return true;
345
-		}
346
-		$role = $attendee->offsetGet('ROLE');
347
-		// @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16
348
-		// Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set
349
-		if ($role === null
350
-			|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0))
351
-			|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0))
352
-		) {
353
-			return true;
354
-		}
355
-
356
-		// RFC 5545 3.2.17: default RSVP is false
357
-		return false;
358
-	}
359
-
360
-	/**
361
-	 * @param IEMailTemplate $template
362
-	 * @param string $method
363
-	 * @param string $sender
364
-	 * @param string $summary
365
-	 * @param string|null $partstat
366
-	 * @param bool $isModified
367
-	 */
368
-	public function addSubjectAndHeading(IEMailTemplate $template,
369
-		string $method, string $sender, string $summary, bool $isModified): void {
370
-		if ($method === IMipPlugin::METHOD_CANCEL) {
371
-			// TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}"
372
-			$template->setSubject($this->l10n->t('Cancelled: %1$s', [$summary]));
373
-			$template->addHeading($this->l10n->t('"%1$s" has been canceled', [$summary]));
374
-		} elseif ($method === IMipPlugin::METHOD_REPLY) {
375
-			// TRANSLATORS Subject for email, when an invitation is replied to. Ex: "Re: {{Event Name}}"
376
-			$template->setSubject($this->l10n->t('Re: %1$s', [$summary]));
377
-			$template->addHeading($this->l10n->t('%1$s has responded to your invitation', [$sender]));
378
-		} elseif ($method === IMipPlugin::METHOD_REQUEST && $isModified) {
379
-			// TRANSLATORS Subject for email, when an invitation is updated. Ex: "Invitation updated: {{Event Name}}"
380
-			$template->setSubject($this->l10n->t('Invitation updated: %1$s', [$summary]));
381
-			$template->addHeading($this->l10n->t('%1$s updated the event "%2$s"', [$sender, $summary]));
382
-		} else {
383
-			// TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}"
384
-			$template->setSubject($this->l10n->t('Invitation: %1$s', [$summary]));
385
-			$template->addHeading($this->l10n->t('%1$s would like to invite you to "%2$s"', [$sender, $summary]));
386
-		}
387
-	}
388
-
389
-	/**
390
-	 * @param string $path
391
-	 * @return string
392
-	 */
393
-	public function getAbsoluteImagePath($path): string {
394
-		return $this->urlGenerator->getAbsoluteURL(
395
-			$this->urlGenerator->imagePath('core', $path)
396
-		);
397
-	}
398
-
399
-	/**
400
-	 * addAttendees: add organizer and attendee names/emails to iMip mail.
401
-	 *
402
-	 * Enable with DAV setting: invitation_list_attendees (default: no)
403
-	 *
404
-	 * The default is 'no', which matches old behavior, and is privacy preserving.
405
-	 *
406
-	 * To enable including attendees in invitation emails:
407
-	 *   % php occ config:app:set dav invitation_list_attendees --value yes
408
-	 *
409
-	 * @param IEMailTemplate $template
410
-	 * @param IL10N $this->l10n
411
-	 * @param VEvent $vevent
412
-	 * @author brad2014 on github.com
413
-	 */
414
-	public function addAttendees(IEMailTemplate $template, VEvent $vevent) {
415
-		if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
416
-			return;
417
-		}
418
-
419
-		if (isset($vevent->ORGANIZER)) {
420
-			/** @var Property | Property\ICalendar\CalAddress $organizer */
421
-			$organizer = $vevent->ORGANIZER;
422
-			$organizerEmail = substr($organizer->getNormalizedValue(), 7);
423
-			/** @var string|null $organizerName */
424
-			$organizerName = isset($organizer->CN) ? $organizer->CN->getValue() : null;
425
-			$organizerHTML = sprintf('<a href="%s">%s</a>',
426
-				htmlspecialchars($organizer->getNormalizedValue()),
427
-				htmlspecialchars($organizerName ?: $organizerEmail));
428
-			$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
429
-			if(isset($organizer['PARTSTAT']) ) {
430
-				/** @var Parameter $partstat */
431
-				$partstat = $organizer['PARTSTAT'];
432
-				if(strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
433
-					$organizerHTML .= ' ✔︎';
434
-					$organizerText .= ' ✔︎';
435
-				}
436
-			}
437
-			$template->addBodyListItem($organizerHTML, $this->l10n->t('Organizer:'),
438
-				$this->getAbsoluteImagePath('caldav/organizer.png'),
439
-				$organizerText, '', IMipPlugin::IMIP_INDENT);
440
-		}
441
-
442
-		$attendees = $vevent->select('ATTENDEE');
443
-		if (count($attendees) === 0) {
444
-			return;
445
-		}
446
-
447
-		$attendeesHTML = [];
448
-		$attendeesText = [];
449
-		foreach ($attendees as $attendee) {
450
-			$attendeeEmail = substr($attendee->getNormalizedValue(), 7);
451
-			$attendeeName = isset($attendee['CN']) ? $attendee['CN']->getValue() : null;
452
-			$attendeeHTML = sprintf('<a href="%s">%s</a>',
453
-				htmlspecialchars($attendee->getNormalizedValue()),
454
-				htmlspecialchars($attendeeName ?: $attendeeEmail));
455
-			$attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
456
-			if (isset($attendee['PARTSTAT'])) {
457
-				/** @var Parameter $partstat */
458
-				$partstat = $attendee['PARTSTAT'];
459
-				if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
460
-					$attendeeHTML .= ' ✔︎';
461
-					$attendeeText .= ' ✔︎';
462
-				}
463
-			}
464
-			$attendeesHTML[] = $attendeeHTML;
465
-			$attendeesText[] = $attendeeText;
466
-		}
467
-
468
-		$template->addBodyListItem(implode('<br/>', $attendeesHTML), $this->l10n->t('Attendees:'),
469
-			$this->getAbsoluteImagePath('caldav/attendees.png'),
470
-			implode("\n", $attendeesText), '', IMipPlugin::IMIP_INDENT);
471
-	}
472
-
473
-	/**
474
-	 * @param IEMailTemplate $template
475
-	 * @param VEVENT $vevent
476
-	 * @param $data
477
-	 */
478
-	public function addBulletList(IEMailTemplate $template, VEvent $vevent, $data) {
479
-		$template->addBodyListItem(
480
-			$data['meeting_title_html'] ?? $data['meeting_title'], $this->l10n->t('Title:'),
481
-			$this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT);
482
-		if ($data['meeting_when'] !== '') {
483
-			$template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Time:'),
484
-				$this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT);
485
-		}
486
-		if ($data['meeting_location'] !== '') {
487
-			$template->addBodyListItem($data['meeting_location_html'] ?? $data['meeting_location'], $this->l10n->t('Location:'),
488
-				$this->getAbsoluteImagePath('caldav/location.png'), $data['meeting_location'], '', IMipPlugin::IMIP_INDENT);
489
-		}
490
-		if ($data['meeting_url'] !== '') {
491
-			$template->addBodyListItem($data['meeting_url_html'] ?? $data['meeting_url'], $this->l10n->t('Link:'),
492
-				$this->getAbsoluteImagePath('caldav/link.png'), $data['meeting_url'], '', IMipPlugin::IMIP_INDENT);
493
-		}
494
-
495
-		$this->addAttendees($template, $vevent);
496
-
497
-		/* Put description last, like an email body, since it can be arbitrarily long */
498
-		if ($data['meeting_description']) {
499
-			$template->addBodyListItem($data['meeting_description_html'] ?? $data['meeting_description'], $this->l10n->t('Description:'),
500
-				$this->getAbsoluteImagePath('caldav/description.png'), $data['meeting_description'], '', IMipPlugin::IMIP_INDENT);
501
-		}
502
-	}
503
-
504
-	/**
505
-	 * @param Message $iTipMessage
506
-	 * @return null|Property
507
-	 */
508
-	public function getCurrentAttendee(Message $iTipMessage): ?Property {
509
-		/** @var VEvent $vevent */
510
-		$vevent = $iTipMessage->message->VEVENT;
511
-		$attendees = $vevent->select('ATTENDEE');
512
-		foreach ($attendees as $attendee) {
513
-			/** @var Property $attendee */
514
-			if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
515
-				return $attendee;
516
-			}
517
-		}
518
-		return null;
519
-	}
520
-
521
-	/**
522
-	 * @param Message $iTipMessage
523
-	 * @param VEvent $vevent
524
-	 * @param int $lastOccurrence
525
-	 * @return string
526
-	 */
527
-	public function createInvitationToken(Message $iTipMessage, VEvent $vevent, int $lastOccurrence): string {
528
-		$token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
529
-
530
-		$attendee = $iTipMessage->recipient;
531
-		$organizer = $iTipMessage->sender;
532
-		$sequence = $iTipMessage->sequence;
533
-		$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
534
-			$vevent->{'RECURRENCE-ID'}->serialize() : null;
535
-		$uid = $vevent->{'UID'};
536
-
537
-		$query = $this->db->getQueryBuilder();
538
-		$query->insert('calendar_invitations')
539
-			->values([
540
-				'token' => $query->createNamedParameter($token),
541
-				'attendee' => $query->createNamedParameter($attendee),
542
-				'organizer' => $query->createNamedParameter($organizer),
543
-				'sequence' => $query->createNamedParameter($sequence),
544
-				'recurrenceid' => $query->createNamedParameter($recurrenceId),
545
-				'expiration' => $query->createNamedParameter($lastOccurrence),
546
-				'uid' => $query->createNamedParameter($uid)
547
-			])
548
-			->execute();
549
-
550
-		return $token;
551
-	}
552
-
553
-	/**
554
-	 * Create a valid VCalendar object out of the details of
555
-	 * a VEvent and its associated iTip Message
556
-	 *
557
-	 * We do this to filter out all unchanged VEvents
558
-	 * This is especially important in iTip Messages with recurrences
559
-	 * and recurrence exceptions
560
-	 *
561
-	 * @param Message $iTipMessage
562
-	 * @param VEvent $vEvent
563
-	 * @return VCalendar
564
-	 */
565
-	public function generateVCalendar(Message $iTipMessage, VEvent $vEvent): VCalendar {
566
-		$vCalendar = new VCalendar();
567
-		$vCalendar->add('METHOD', $iTipMessage->method);
568
-		foreach ($iTipMessage->message->getComponents() as $component) {
569
-			if ($component instanceof VEvent) {
570
-				continue;
571
-			}
572
-			$vCalendar->add(clone $component);
573
-		}
574
-		$vCalendar->add($vEvent);
575
-		return $vCalendar;
576
-	}
577
-
578
-	/**
579
-	 * @param IEMailTemplate $template
580
-	 * @param $token
581
-	 */
582
-	public function addResponseButtons(IEMailTemplate $template, $token) {
583
-		$template->addBodyButtonGroup(
584
-			$this->l10n->t('Accept'),
585
-			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
586
-				'token' => $token,
587
-			]),
588
-			$this->l10n->t('Decline'),
589
-			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
590
-				'token' => $token,
591
-			])
592
-		);
593
-	}
594
-
595
-	public function addMoreOptionsButton(IEMailTemplate $template, $token) {
596
-		$moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
597
-			'token' => $token,
598
-		]);
599
-		$html = vsprintf('<small><a href="%s">%s</a></small>', [
600
-			$moreOptionsURL, $this->l10n->t('More options …')
601
-		]);
602
-		$text = $this->l10n->t('More options at %s', [$moreOptionsURL]);
603
-
604
-		$template->addBodyText($html, $text);
605
-	}
43
+    private URLGenerator $urlGenerator;
44
+    private IConfig $config;
45
+    private IDBConnection $db;
46
+    private ISecureRandom $random;
47
+    private L10NFactory $l10nFactory;
48
+    private IL10N $l10n;
49
+
50
+    /** @var string[] */
51
+    private const STRING_DIFF = [
52
+        'meeting_title' => 'SUMMARY',
53
+        'meeting_description' => 'DESCRIPTION',
54
+        'meeting_url' => 'URL',
55
+        'meeting_location' => 'LOCATION'
56
+    ];
57
+
58
+    public function __construct(URLGenerator $urlGenerator,
59
+                                IConfig $config,
60
+                                IDBConnection $db,
61
+                                ISecureRandom $random,
62
+                                L10NFactory $l10nFactory) {
63
+        $this->urlGenerator = $urlGenerator;
64
+        $this->config = $config;
65
+        $this->db = $db;
66
+        $this->random = $random;
67
+        $this->l10nFactory = $l10nFactory;
68
+        $default = $this->l10nFactory->findGenericLanguage();
69
+        $this->l10n = $this->l10nFactory->get('dav', $default);
70
+    }
71
+
72
+    /**
73
+     * @param string|null $senderName
74
+     * @param string $default
75
+     * @return string
76
+     */
77
+    public function getFrom(?string $senderName, string $default): string {
78
+        if ($senderName === null) {
79
+            return $default;
80
+        }
81
+
82
+        return $this->l10n->t('%1$s via %2$s', [$senderName, $default]);
83
+    }
84
+
85
+    public static function readPropertyWithDefault(VEvent $vevent, string $property, string $default) {
86
+        if (isset($vevent->$property)) {
87
+            $value = $vevent->$property->getValue();
88
+            if (!empty($value)) {
89
+                return $value;
90
+            }
91
+        }
92
+        return $default;
93
+    }
94
+
95
+    private function generateDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
96
+        $strikethrough = "<span style='text-decoration: line-through'>%s</span><br />%s";
97
+        if (!isset($vevent->$property)) {
98
+            return $default;
99
+        }
100
+        $newstring = $vevent->$property->getValue();
101
+        if(isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring ) {
102
+            $oldstring = $oldVEvent->$property->getValue();
103
+            return sprintf($strikethrough, $oldstring, $newstring);
104
+        }
105
+        return $newstring;
106
+    }
107
+
108
+    /**
109
+     * @param VEvent $vEvent
110
+     * @param VEvent|null $oldVEvent
111
+     * @return array
112
+     */
113
+    public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array {
114
+        $defaultVal = '';
115
+        $data = [];
116
+        $data['meeting_when'] = $this->generateWhenString($vEvent);
117
+
118
+        foreach(self::STRING_DIFF as $key => $property) {
119
+            $data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
120
+        }
121
+
122
+        $data['meeting_url_html'] = self::readPropertyWithDefault($vEvent, 'URL', $defaultVal);
123
+
124
+        if(!empty($oldVEvent)) {
125
+            $oldMeetingWhen = $this->generateWhenString($oldVEvent);
126
+            $data['meeting_title_html']	= $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
127
+            $data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
128
+            $data['meeting_location_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
129
+
130
+            $oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
131
+            $data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url'];
132
+
133
+            $data['meeting_when_html'] =
134
+                ($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null)
135
+                    ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when'])
136
+                    : $data['meeting_when'];
137
+        }
138
+        return $data;
139
+    }
140
+
141
+    /**
142
+     * @param IL10N $this->l10n
143
+     * @param VEvent $vevent
144
+     * @return false|int|string
145
+     */
146
+    public function generateWhenString(VEvent $vevent) {
147
+        /** @var Property\ICalendar\DateTime $dtstart */
148
+        $dtstart = $vevent->DTSTART;
149
+        if (isset($vevent->DTEND)) {
150
+            /** @var Property\ICalendar\DateTime $dtend */
151
+            $dtend = $vevent->DTEND;
152
+        } elseif (isset($vevent->DURATION)) {
153
+            $isFloating = $dtstart->isFloating();
154
+            $dtend = clone $dtstart;
155
+            $endDateTime = $dtend->getDateTime();
156
+            $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
157
+            $dtend->setDateTime($endDateTime, $isFloating);
158
+        } elseif (!$dtstart->hasTime()) {
159
+            $isFloating = $dtstart->isFloating();
160
+            $dtend = clone $dtstart;
161
+            $endDateTime = $dtend->getDateTime();
162
+            $endDateTime = $endDateTime->modify('+1 day');
163
+            $dtend->setDateTime($endDateTime, $isFloating);
164
+        } else {
165
+            $dtend = clone $dtstart;
166
+        }
167
+
168
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
169
+        /** @var \DateTimeImmutable $dtstartDt */
170
+        $dtstartDt = $dtstart->getDateTime();
171
+
172
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
173
+        /** @var \DateTimeImmutable $dtendDt */
174
+        $dtendDt = $dtend->getDateTime();
175
+
176
+        $diff = $dtstartDt->diff($dtendDt);
177
+
178
+        $dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
179
+        $dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
180
+
181
+        if ($dtstart instanceof Property\ICalendar\Date) {
182
+            // One day event
183
+            if ($diff->days === 1) {
184
+                return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
185
+            }
186
+
187
+            // DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
188
+            // the email should show 2020-01-01 to 2020-01-04.
189
+            $dtendDt->modify('-1 day');
190
+
191
+            //event that spans over multiple days
192
+            $localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
193
+            $localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']);
194
+
195
+            return $localeStart . ' - ' . $localeEnd;
196
+        }
197
+
198
+        /** @var Property\ICalendar\DateTime $dtstart */
199
+        /** @var Property\ICalendar\DateTime $dtend */
200
+        $isFloating = $dtstart->isFloating();
201
+        $startTimezone = $endTimezone = null;
202
+        if (!$isFloating) {
203
+            $prop = $dtstart->offsetGet('TZID');
204
+            if ($prop instanceof Parameter) {
205
+                $startTimezone = $prop->getValue();
206
+            }
207
+
208
+            $prop = $dtend->offsetGet('TZID');
209
+            if ($prop instanceof Parameter) {
210
+                $endTimezone = $prop->getValue();
211
+            }
212
+        }
213
+
214
+        $localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
215
+            $this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
216
+
217
+        // always show full date with timezone if timezones are different
218
+        if ($startTimezone !== $endTimezone) {
219
+            $localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
220
+
221
+            return $localeStart . ' (' . $startTimezone . ') - ' .
222
+                $localeEnd . ' (' . $endTimezone . ')';
223
+        }
224
+
225
+        // show only end time if date is the same
226
+        if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) {
227
+            $localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']);
228
+        } else {
229
+            $localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
230
+                $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
231
+        }
232
+
233
+        return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
234
+    }
235
+
236
+    /**
237
+     * @param VEvent $vEvent
238
+     * @return array
239
+     */
240
+    public function buildCancelledBodyData(VEvent $vEvent): array {
241
+        $defaultVal = '';
242
+        $strikethrough = "<span style='text-decoration: line-through'>%s</span>";
243
+
244
+        $newMeetingWhen = $this->generateWhenString($vEvent);
245
+        $newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');;
246
+        $newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
247
+        $newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
248
+        $newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
249
+
250
+        $data = [];
251
+        $data['meeting_when_html'] = $newMeetingWhen === '' ?: sprintf($strikethrough, $newMeetingWhen);
252
+        $data['meeting_when'] = $newMeetingWhen;
253
+        $data['meeting_title_html'] = sprintf($strikethrough, $newSummary);
254
+        $data['meeting_title'] = $newSummary !== '' ? $newSummary: $this->l10n->t('Untitled event');
255
+        $data['meeting_description_html'] = $newDescription !== '' ? sprintf($strikethrough, $newDescription) : '';
256
+        $data['meeting_description'] = $newDescription;
257
+        $data['meeting_url_html'] = $newUrl !== '' ? sprintf($strikethrough, $newUrl) : '';
258
+        $data['meeting_url'] = isset($vEvent->URL) ? (string)$vEvent->URL : '';
259
+        $data['meeting_location_html'] = $newLocation !== '' ? sprintf($strikethrough, $newLocation) : '';
260
+        $data['meeting_location'] = $newLocation;
261
+        return $data;
262
+    }
263
+
264
+    /**
265
+     * Check if event took place in the past
266
+     *
267
+     * @param VCalendar $vObject
268
+     * @return int
269
+     */
270
+    public function getLastOccurrence(VCalendar $vObject) {
271
+        /** @var VEvent $component */
272
+        $component = $vObject->VEVENT;
273
+
274
+        if (isset($component->RRULE)) {
275
+            $it = new EventIterator($vObject, (string)$component->UID);
276
+            $maxDate = new \DateTime(IMipPlugin::MAX_DATE);
277
+            if ($it->isInfinite()) {
278
+                return $maxDate->getTimestamp();
279
+            }
280
+
281
+            $end = $it->getDtEnd();
282
+            while ($it->valid() && $end < $maxDate) {
283
+                $end = $it->getDtEnd();
284
+                $it->next();
285
+            }
286
+            return $end->getTimestamp();
287
+        }
288
+
289
+        /** @var Property\ICalendar\DateTime $dtStart */
290
+        $dtStart = $component->DTSTART;
291
+
292
+        if (isset($component->DTEND)) {
293
+            /** @var Property\ICalendar\DateTime $dtEnd */
294
+            $dtEnd = $component->DTEND;
295
+            return $dtEnd->getDateTime()->getTimeStamp();
296
+        }
297
+
298
+        if(isset($component->DURATION)) {
299
+            /** @var \DateTime $endDate */
300
+            $endDate = clone $dtStart->getDateTime();
301
+            // $component->DTEND->getDateTime() returns DateTimeImmutable
302
+            $endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
303
+            return $endDate->getTimestamp();
304
+        }
305
+
306
+        if(!$dtStart->hasTime()) {
307
+            /** @var \DateTime $endDate */
308
+            // $component->DTSTART->getDateTime() returns DateTimeImmutable
309
+            $endDate = clone $dtStart->getDateTime();
310
+            $endDate = $endDate->modify('+1 day');
311
+            return $endDate->getTimestamp();
312
+        }
313
+
314
+        // No computation of end time possible - return start date
315
+        return $dtStart->getDateTime()->getTimeStamp();
316
+    }
317
+
318
+    /**
319
+     * @param Property|null $attendee
320
+     */
321
+    public function setL10n(?Property $attendee = null) {
322
+        if($attendee === null) {
323
+            return;
324
+        }
325
+
326
+        $lang = $attendee->offsetGet('LANGUAGE');
327
+        if ($lang instanceof Parameter) {
328
+            $lang = $lang->getValue();
329
+            $this->l10n = $this->l10nFactory->get('dav', $lang);
330
+        }
331
+    }
332
+
333
+    /**
334
+     * @param Property|null $attendee
335
+     * @return bool
336
+     */
337
+    public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) {
338
+        if($attendee === null) {
339
+            return false;
340
+        }
341
+
342
+        $rsvp = $attendee->offsetGet('RSVP');
343
+        if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
344
+            return true;
345
+        }
346
+        $role = $attendee->offsetGet('ROLE');
347
+        // @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16
348
+        // Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set
349
+        if ($role === null
350
+            || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0))
351
+            || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0))
352
+        ) {
353
+            return true;
354
+        }
355
+
356
+        // RFC 5545 3.2.17: default RSVP is false
357
+        return false;
358
+    }
359
+
360
+    /**
361
+     * @param IEMailTemplate $template
362
+     * @param string $method
363
+     * @param string $sender
364
+     * @param string $summary
365
+     * @param string|null $partstat
366
+     * @param bool $isModified
367
+     */
368
+    public function addSubjectAndHeading(IEMailTemplate $template,
369
+        string $method, string $sender, string $summary, bool $isModified): void {
370
+        if ($method === IMipPlugin::METHOD_CANCEL) {
371
+            // TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}"
372
+            $template->setSubject($this->l10n->t('Cancelled: %1$s', [$summary]));
373
+            $template->addHeading($this->l10n->t('"%1$s" has been canceled', [$summary]));
374
+        } elseif ($method === IMipPlugin::METHOD_REPLY) {
375
+            // TRANSLATORS Subject for email, when an invitation is replied to. Ex: "Re: {{Event Name}}"
376
+            $template->setSubject($this->l10n->t('Re: %1$s', [$summary]));
377
+            $template->addHeading($this->l10n->t('%1$s has responded to your invitation', [$sender]));
378
+        } elseif ($method === IMipPlugin::METHOD_REQUEST && $isModified) {
379
+            // TRANSLATORS Subject for email, when an invitation is updated. Ex: "Invitation updated: {{Event Name}}"
380
+            $template->setSubject($this->l10n->t('Invitation updated: %1$s', [$summary]));
381
+            $template->addHeading($this->l10n->t('%1$s updated the event "%2$s"', [$sender, $summary]));
382
+        } else {
383
+            // TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}"
384
+            $template->setSubject($this->l10n->t('Invitation: %1$s', [$summary]));
385
+            $template->addHeading($this->l10n->t('%1$s would like to invite you to "%2$s"', [$sender, $summary]));
386
+        }
387
+    }
388
+
389
+    /**
390
+     * @param string $path
391
+     * @return string
392
+     */
393
+    public function getAbsoluteImagePath($path): string {
394
+        return $this->urlGenerator->getAbsoluteURL(
395
+            $this->urlGenerator->imagePath('core', $path)
396
+        );
397
+    }
398
+
399
+    /**
400
+     * addAttendees: add organizer and attendee names/emails to iMip mail.
401
+     *
402
+     * Enable with DAV setting: invitation_list_attendees (default: no)
403
+     *
404
+     * The default is 'no', which matches old behavior, and is privacy preserving.
405
+     *
406
+     * To enable including attendees in invitation emails:
407
+     *   % php occ config:app:set dav invitation_list_attendees --value yes
408
+     *
409
+     * @param IEMailTemplate $template
410
+     * @param IL10N $this->l10n
411
+     * @param VEvent $vevent
412
+     * @author brad2014 on github.com
413
+     */
414
+    public function addAttendees(IEMailTemplate $template, VEvent $vevent) {
415
+        if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
416
+            return;
417
+        }
418
+
419
+        if (isset($vevent->ORGANIZER)) {
420
+            /** @var Property | Property\ICalendar\CalAddress $organizer */
421
+            $organizer = $vevent->ORGANIZER;
422
+            $organizerEmail = substr($organizer->getNormalizedValue(), 7);
423
+            /** @var string|null $organizerName */
424
+            $organizerName = isset($organizer->CN) ? $organizer->CN->getValue() : null;
425
+            $organizerHTML = sprintf('<a href="%s">%s</a>',
426
+                htmlspecialchars($organizer->getNormalizedValue()),
427
+                htmlspecialchars($organizerName ?: $organizerEmail));
428
+            $organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
429
+            if(isset($organizer['PARTSTAT']) ) {
430
+                /** @var Parameter $partstat */
431
+                $partstat = $organizer['PARTSTAT'];
432
+                if(strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
433
+                    $organizerHTML .= ' ✔︎';
434
+                    $organizerText .= ' ✔︎';
435
+                }
436
+            }
437
+            $template->addBodyListItem($organizerHTML, $this->l10n->t('Organizer:'),
438
+                $this->getAbsoluteImagePath('caldav/organizer.png'),
439
+                $organizerText, '', IMipPlugin::IMIP_INDENT);
440
+        }
441
+
442
+        $attendees = $vevent->select('ATTENDEE');
443
+        if (count($attendees) === 0) {
444
+            return;
445
+        }
446
+
447
+        $attendeesHTML = [];
448
+        $attendeesText = [];
449
+        foreach ($attendees as $attendee) {
450
+            $attendeeEmail = substr($attendee->getNormalizedValue(), 7);
451
+            $attendeeName = isset($attendee['CN']) ? $attendee['CN']->getValue() : null;
452
+            $attendeeHTML = sprintf('<a href="%s">%s</a>',
453
+                htmlspecialchars($attendee->getNormalizedValue()),
454
+                htmlspecialchars($attendeeName ?: $attendeeEmail));
455
+            $attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
456
+            if (isset($attendee['PARTSTAT'])) {
457
+                /** @var Parameter $partstat */
458
+                $partstat = $attendee['PARTSTAT'];
459
+                if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
460
+                    $attendeeHTML .= ' ✔︎';
461
+                    $attendeeText .= ' ✔︎';
462
+                }
463
+            }
464
+            $attendeesHTML[] = $attendeeHTML;
465
+            $attendeesText[] = $attendeeText;
466
+        }
467
+
468
+        $template->addBodyListItem(implode('<br/>', $attendeesHTML), $this->l10n->t('Attendees:'),
469
+            $this->getAbsoluteImagePath('caldav/attendees.png'),
470
+            implode("\n", $attendeesText), '', IMipPlugin::IMIP_INDENT);
471
+    }
472
+
473
+    /**
474
+     * @param IEMailTemplate $template
475
+     * @param VEVENT $vevent
476
+     * @param $data
477
+     */
478
+    public function addBulletList(IEMailTemplate $template, VEvent $vevent, $data) {
479
+        $template->addBodyListItem(
480
+            $data['meeting_title_html'] ?? $data['meeting_title'], $this->l10n->t('Title:'),
481
+            $this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT);
482
+        if ($data['meeting_when'] !== '') {
483
+            $template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Time:'),
484
+                $this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT);
485
+        }
486
+        if ($data['meeting_location'] !== '') {
487
+            $template->addBodyListItem($data['meeting_location_html'] ?? $data['meeting_location'], $this->l10n->t('Location:'),
488
+                $this->getAbsoluteImagePath('caldav/location.png'), $data['meeting_location'], '', IMipPlugin::IMIP_INDENT);
489
+        }
490
+        if ($data['meeting_url'] !== '') {
491
+            $template->addBodyListItem($data['meeting_url_html'] ?? $data['meeting_url'], $this->l10n->t('Link:'),
492
+                $this->getAbsoluteImagePath('caldav/link.png'), $data['meeting_url'], '', IMipPlugin::IMIP_INDENT);
493
+        }
494
+
495
+        $this->addAttendees($template, $vevent);
496
+
497
+        /* Put description last, like an email body, since it can be arbitrarily long */
498
+        if ($data['meeting_description']) {
499
+            $template->addBodyListItem($data['meeting_description_html'] ?? $data['meeting_description'], $this->l10n->t('Description:'),
500
+                $this->getAbsoluteImagePath('caldav/description.png'), $data['meeting_description'], '', IMipPlugin::IMIP_INDENT);
501
+        }
502
+    }
503
+
504
+    /**
505
+     * @param Message $iTipMessage
506
+     * @return null|Property
507
+     */
508
+    public function getCurrentAttendee(Message $iTipMessage): ?Property {
509
+        /** @var VEvent $vevent */
510
+        $vevent = $iTipMessage->message->VEVENT;
511
+        $attendees = $vevent->select('ATTENDEE');
512
+        foreach ($attendees as $attendee) {
513
+            /** @var Property $attendee */
514
+            if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
515
+                return $attendee;
516
+            }
517
+        }
518
+        return null;
519
+    }
520
+
521
+    /**
522
+     * @param Message $iTipMessage
523
+     * @param VEvent $vevent
524
+     * @param int $lastOccurrence
525
+     * @return string
526
+     */
527
+    public function createInvitationToken(Message $iTipMessage, VEvent $vevent, int $lastOccurrence): string {
528
+        $token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
529
+
530
+        $attendee = $iTipMessage->recipient;
531
+        $organizer = $iTipMessage->sender;
532
+        $sequence = $iTipMessage->sequence;
533
+        $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
534
+            $vevent->{'RECURRENCE-ID'}->serialize() : null;
535
+        $uid = $vevent->{'UID'};
536
+
537
+        $query = $this->db->getQueryBuilder();
538
+        $query->insert('calendar_invitations')
539
+            ->values([
540
+                'token' => $query->createNamedParameter($token),
541
+                'attendee' => $query->createNamedParameter($attendee),
542
+                'organizer' => $query->createNamedParameter($organizer),
543
+                'sequence' => $query->createNamedParameter($sequence),
544
+                'recurrenceid' => $query->createNamedParameter($recurrenceId),
545
+                'expiration' => $query->createNamedParameter($lastOccurrence),
546
+                'uid' => $query->createNamedParameter($uid)
547
+            ])
548
+            ->execute();
549
+
550
+        return $token;
551
+    }
552
+
553
+    /**
554
+     * Create a valid VCalendar object out of the details of
555
+     * a VEvent and its associated iTip Message
556
+     *
557
+     * We do this to filter out all unchanged VEvents
558
+     * This is especially important in iTip Messages with recurrences
559
+     * and recurrence exceptions
560
+     *
561
+     * @param Message $iTipMessage
562
+     * @param VEvent $vEvent
563
+     * @return VCalendar
564
+     */
565
+    public function generateVCalendar(Message $iTipMessage, VEvent $vEvent): VCalendar {
566
+        $vCalendar = new VCalendar();
567
+        $vCalendar->add('METHOD', $iTipMessage->method);
568
+        foreach ($iTipMessage->message->getComponents() as $component) {
569
+            if ($component instanceof VEvent) {
570
+                continue;
571
+            }
572
+            $vCalendar->add(clone $component);
573
+        }
574
+        $vCalendar->add($vEvent);
575
+        return $vCalendar;
576
+    }
577
+
578
+    /**
579
+     * @param IEMailTemplate $template
580
+     * @param $token
581
+     */
582
+    public function addResponseButtons(IEMailTemplate $template, $token) {
583
+        $template->addBodyButtonGroup(
584
+            $this->l10n->t('Accept'),
585
+            $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
586
+                'token' => $token,
587
+            ]),
588
+            $this->l10n->t('Decline'),
589
+            $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
590
+                'token' => $token,
591
+            ])
592
+        );
593
+    }
594
+
595
+    public function addMoreOptionsButton(IEMailTemplate $template, $token) {
596
+        $moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
597
+            'token' => $token,
598
+        ]);
599
+        $html = vsprintf('<small><a href="%s">%s</a></small>', [
600
+            $moreOptionsURL, $this->l10n->t('More options …')
601
+        ]);
602
+        $text = $this->l10n->t('More options at %s', [$moreOptionsURL]);
603
+
604
+        $template->addBodyText($html, $text);
605
+    }
606 606
 }
Please login to merge, or discard this patch.
Spacing   +23 added lines, -23 removed lines patch added patch discarded remove patch
@@ -98,7 +98,7 @@  discard block
 block discarded – undo
98 98
 			return $default;
99 99
 		}
100 100
 		$newstring = $vevent->$property->getValue();
101
-		if(isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring ) {
101
+		if (isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) {
102 102
 			$oldstring = $oldVEvent->$property->getValue();
103 103
 			return sprintf($strikethrough, $oldstring, $newstring);
104 104
 		}
@@ -115,15 +115,15 @@  discard block
 block discarded – undo
115 115
 		$data = [];
116 116
 		$data['meeting_when'] = $this->generateWhenString($vEvent);
117 117
 
118
-		foreach(self::STRING_DIFF as $key => $property) {
118
+		foreach (self::STRING_DIFF as $key => $property) {
119 119
 			$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
120 120
 		}
121 121
 
122 122
 		$data['meeting_url_html'] = self::readPropertyWithDefault($vEvent, 'URL', $defaultVal);
123 123
 
124
-		if(!empty($oldVEvent)) {
124
+		if (!empty($oldVEvent)) {
125 125
 			$oldMeetingWhen = $this->generateWhenString($oldVEvent);
126
-			$data['meeting_title_html']	= $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
126
+			$data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
127 127
 			$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
128 128
 			$data['meeting_location_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
129 129
 
@@ -192,7 +192,7 @@  discard block
 block discarded – undo
192 192
 			$localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
193 193
 			$localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']);
194 194
 
195
-			return $localeStart . ' - ' . $localeEnd;
195
+			return $localeStart.' - '.$localeEnd;
196 196
 		}
197 197
 
198 198
 		/** @var Property\ICalendar\DateTime $dtstart */
@@ -211,26 +211,26 @@  discard block
 block discarded – undo
211 211
 			}
212 212
 		}
213 213
 
214
-		$localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
214
+		$localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']).', '.
215 215
 			$this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
216 216
 
217 217
 		// always show full date with timezone if timezones are different
218 218
 		if ($startTimezone !== $endTimezone) {
219 219
 			$localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
220 220
 
221
-			return $localeStart . ' (' . $startTimezone . ') - ' .
222
-				$localeEnd . ' (' . $endTimezone . ')';
221
+			return $localeStart.' ('.$startTimezone.') - '.
222
+				$localeEnd.' ('.$endTimezone.')';
223 223
 		}
224 224
 
225 225
 		// show only end time if date is the same
226 226
 		if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) {
227 227
 			$localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']);
228 228
 		} else {
229
-			$localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
229
+			$localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']).', '.
230 230
 				$this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
231 231
 		}
232 232
 
233
-		return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
233
+		return $localeStart.' - '.$localeEnd.' ('.$startTimezone.')';
234 234
 	}
235 235
 
236 236
 	/**
@@ -242,20 +242,20 @@  discard block
 block discarded – undo
242 242
 		$strikethrough = "<span style='text-decoration: line-through'>%s</span>";
243 243
 
244 244
 		$newMeetingWhen = $this->generateWhenString($vEvent);
245
-		$newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');;
246
-		$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
247
-		$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
248
-		$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
245
+		$newSummary = isset($vEvent->SUMMARY) && (string) $vEvent->SUMMARY !== '' ? (string) $vEvent->SUMMARY : $this->l10n->t('Untitled event'); ;
246
+		$newDescription = isset($vEvent->DESCRIPTION) && (string) $vEvent->DESCRIPTION !== '' ? (string) $vEvent->DESCRIPTION : $defaultVal;
247
+		$newUrl = isset($vEvent->URL) && (string) $vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
248
+		$newLocation = isset($vEvent->LOCATION) && (string) $vEvent->LOCATION !== '' ? (string) $vEvent->LOCATION : $defaultVal;
249 249
 
250 250
 		$data = [];
251 251
 		$data['meeting_when_html'] = $newMeetingWhen === '' ?: sprintf($strikethrough, $newMeetingWhen);
252 252
 		$data['meeting_when'] = $newMeetingWhen;
253 253
 		$data['meeting_title_html'] = sprintf($strikethrough, $newSummary);
254
-		$data['meeting_title'] = $newSummary !== '' ? $newSummary: $this->l10n->t('Untitled event');
254
+		$data['meeting_title'] = $newSummary !== '' ? $newSummary : $this->l10n->t('Untitled event');
255 255
 		$data['meeting_description_html'] = $newDescription !== '' ? sprintf($strikethrough, $newDescription) : '';
256 256
 		$data['meeting_description'] = $newDescription;
257 257
 		$data['meeting_url_html'] = $newUrl !== '' ? sprintf($strikethrough, $newUrl) : '';
258
-		$data['meeting_url'] = isset($vEvent->URL) ? (string)$vEvent->URL : '';
258
+		$data['meeting_url'] = isset($vEvent->URL) ? (string) $vEvent->URL : '';
259 259
 		$data['meeting_location_html'] = $newLocation !== '' ? sprintf($strikethrough, $newLocation) : '';
260 260
 		$data['meeting_location'] = $newLocation;
261 261
 		return $data;
@@ -272,7 +272,7 @@  discard block
 block discarded – undo
272 272
 		$component = $vObject->VEVENT;
273 273
 
274 274
 		if (isset($component->RRULE)) {
275
-			$it = new EventIterator($vObject, (string)$component->UID);
275
+			$it = new EventIterator($vObject, (string) $component->UID);
276 276
 			$maxDate = new \DateTime(IMipPlugin::MAX_DATE);
277 277
 			if ($it->isInfinite()) {
278 278
 				return $maxDate->getTimestamp();
@@ -295,7 +295,7 @@  discard block
 block discarded – undo
295 295
 			return $dtEnd->getDateTime()->getTimeStamp();
296 296
 		}
297 297
 
298
-		if(isset($component->DURATION)) {
298
+		if (isset($component->DURATION)) {
299 299
 			/** @var \DateTime $endDate */
300 300
 			$endDate = clone $dtStart->getDateTime();
301 301
 			// $component->DTEND->getDateTime() returns DateTimeImmutable
@@ -303,7 +303,7 @@  discard block
 block discarded – undo
303 303
 			return $endDate->getTimestamp();
304 304
 		}
305 305
 
306
-		if(!$dtStart->hasTime()) {
306
+		if (!$dtStart->hasTime()) {
307 307
 			/** @var \DateTime $endDate */
308 308
 			// $component->DTSTART->getDateTime() returns DateTimeImmutable
309 309
 			$endDate = clone $dtStart->getDateTime();
@@ -319,7 +319,7 @@  discard block
 block discarded – undo
319 319
 	 * @param Property|null $attendee
320 320
 	 */
321 321
 	public function setL10n(?Property $attendee = null) {
322
-		if($attendee === null) {
322
+		if ($attendee === null) {
323 323
 			return;
324 324
 		}
325 325
 
@@ -335,7 +335,7 @@  discard block
 block discarded – undo
335 335
 	 * @return bool
336 336
 	 */
337 337
 	public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) {
338
-		if($attendee === null) {
338
+		if ($attendee === null) {
339 339
 			return false;
340 340
 		}
341 341
 
@@ -426,10 +426,10 @@  discard block
 block discarded – undo
426 426
 				htmlspecialchars($organizer->getNormalizedValue()),
427 427
 				htmlspecialchars($organizerName ?: $organizerEmail));
428 428
 			$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
429
-			if(isset($organizer['PARTSTAT']) ) {
429
+			if (isset($organizer['PARTSTAT'])) {
430 430
 				/** @var Parameter $partstat */
431 431
 				$partstat = $organizer['PARTSTAT'];
432
-				if(strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
432
+				if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
433 433
 					$organizerHTML .= ' ✔︎';
434 434
 					$organizerText .= ' ✔︎';
435 435
 				}
Please login to merge, or discard this patch.