Passed
Push — master ( 37199c...f124cb )
by Roeland
23:12 queued 08:03
created
apps/dav/lib/CalDAV/Schedule/IMipPlugin.php 1 patch
Indentation   +622 added lines, -622 removed lines patch added patch discarded remove patch
@@ -70,191 +70,191 @@  discard block
 block discarded – undo
70 70
  */
71 71
 class IMipPlugin extends SabreIMipPlugin {
72 72
 
73
-	/** @var string */
74
-	private $userId;
75
-
76
-	/** @var IConfig */
77
-	private $config;
78
-
79
-	/** @var IMailer */
80
-	private $mailer;
81
-
82
-	/** @var ILogger */
83
-	private $logger;
84
-
85
-	/** @var ITimeFactory */
86
-	private $timeFactory;
87
-
88
-	/** @var L10NFactory */
89
-	private $l10nFactory;
90
-
91
-	/** @var IURLGenerator */
92
-	private $urlGenerator;
93
-
94
-	/** @var ISecureRandom */
95
-	private $random;
96
-
97
-	/** @var IDBConnection */
98
-	private $db;
99
-
100
-	/** @var Defaults */
101
-	private $defaults;
102
-
103
-	/** @var IUserManager */
104
-	private $userManager;
105
-
106
-	public const MAX_DATE = '2038-01-01';
107
-
108
-	public const METHOD_REQUEST = 'request';
109
-	public const METHOD_REPLY = 'reply';
110
-	public const METHOD_CANCEL = 'cancel';
111
-	public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages
112
-
113
-	/**
114
-	 * @param IConfig $config
115
-	 * @param IMailer $mailer
116
-	 * @param ILogger $logger
117
-	 * @param ITimeFactory $timeFactory
118
-	 * @param L10NFactory $l10nFactory
119
-	 * @param IUrlGenerator $urlGenerator
120
-	 * @param Defaults $defaults
121
-	 * @param ISecureRandom $random
122
-	 * @param IDBConnection $db
123
-	 * @param string $userId
124
-	 */
125
-	public function __construct(IConfig $config, IMailer $mailer, ILogger $logger,
126
-								ITimeFactory $timeFactory, L10NFactory $l10nFactory,
127
-								IURLGenerator $urlGenerator, Defaults $defaults,
128
-								ISecureRandom $random, IDBConnection $db, IUserManager $userManager,
129
-								$userId) {
130
-		parent::__construct('');
131
-		$this->userId = $userId;
132
-		$this->config = $config;
133
-		$this->mailer = $mailer;
134
-		$this->logger = $logger;
135
-		$this->timeFactory = $timeFactory;
136
-		$this->l10nFactory = $l10nFactory;
137
-		$this->urlGenerator = $urlGenerator;
138
-		$this->random = $random;
139
-		$this->db = $db;
140
-		$this->defaults = $defaults;
141
-		$this->userManager = $userManager;
142
-	}
143
-
144
-	/**
145
-	 * Event handler for the 'schedule' event.
146
-	 *
147
-	 * @param Message $iTipMessage
148
-	 * @return void
149
-	 */
150
-	public function schedule(Message $iTipMessage) {
151
-
152
-		// Not sending any emails if the system considers the update
153
-		// insignificant.
154
-		if (!$iTipMessage->significantChange) {
155
-			if (!$iTipMessage->scheduleStatus) {
156
-				$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
157
-			}
158
-			return;
159
-		}
160
-
161
-		$summary = $iTipMessage->message->VEVENT->SUMMARY;
162
-
163
-		if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') {
164
-			return;
165
-		}
166
-
167
-		if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
168
-			return;
169
-		}
170
-
171
-		// don't send out mails for events that already took place
172
-		$lastOccurrence = $this->getLastOccurrence($iTipMessage->message);
173
-		$currentTime = $this->timeFactory->getTime();
174
-		if ($lastOccurrence < $currentTime) {
175
-			return;
176
-		}
177
-
178
-		// Strip off mailto:
179
-		$sender = substr($iTipMessage->sender, 7);
180
-		$recipient = substr($iTipMessage->recipient, 7);
181
-		if (!$this->mailer->validateMailAddress($recipient)) {
182
-			// Nothing to send if the recipient doesn't have a valid email address
183
-			$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
184
-			return;
185
-		}
186
-
187
-		$senderName = $iTipMessage->senderName ?: null;
188
-		$recipientName = $iTipMessage->recipientName ?: null;
189
-
190
-		if ($senderName === null || empty(trim($senderName))) {
191
-			$user = $this->userManager->get($this->userId);
192
-			if ($user) {
193
-				// getDisplayName automatically uses the uid
194
-				// if no display-name is set
195
-				$senderName = $user->getDisplayName();
196
-			}
197
-		}
198
-
199
-		/** @var VEvent $vevent */
200
-		$vevent = $iTipMessage->message->VEVENT;
201
-
202
-		$attendee = $this->getCurrentAttendee($iTipMessage);
203
-		$defaultLang = $this->l10nFactory->findLanguage();
204
-		$lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee);
205
-		$l10n = $this->l10nFactory->get('dav', $lang);
206
-
207
-		$meetingAttendeeName = $recipientName ?: $recipient;
208
-		$meetingInviteeName = $senderName ?: $sender;
209
-
210
-		$meetingTitle = $vevent->SUMMARY;
211
-		$meetingDescription = $vevent->DESCRIPTION;
212
-
213
-
214
-		$meetingUrl = $vevent->URL;
215
-		$meetingLocation = $vevent->LOCATION;
216
-
217
-		$defaultVal = '--';
218
-
219
-		$method = self::METHOD_REQUEST;
220
-		switch (strtolower($iTipMessage->method)) {
221
-			case self::METHOD_REPLY:
222
-				$method = self::METHOD_REPLY;
223
-				break;
224
-			case self::METHOD_CANCEL:
225
-				$method = self::METHOD_CANCEL;
226
-				break;
227
-		}
228
-
229
-		$data = [
230
-			'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
231
-			'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
232
-			'meeting_title' => (string)$meetingTitle ?: $defaultVal,
233
-			'meeting_description' => (string)$meetingDescription ?: $defaultVal,
234
-			'meeting_url' => (string)$meetingUrl ?: $defaultVal,
235
-		];
236
-
237
-		$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
238
-		$fromName = $l10n->t('%1$s via %2$s', [$senderName, $this->defaults->getName()]);
239
-
240
-		$message = $this->mailer->createMessage()
241
-			->setFrom([$fromEMail => $fromName])
242
-			->setReplyTo([$sender => $senderName])
243
-			->setTo([$recipient => $recipientName]);
244
-
245
-		$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
246
-		$template->addHeader();
247
-
248
-		$summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
249
-
250
-		$this->addSubjectAndHeading($template, $l10n, $method, $summary);
251
-		$this->addBulletList($template, $l10n, $vevent);
252
-
253
-
254
-		// Only add response buttons to invitation requests: Fix Issue #11230
255
-		if (($method == self::METHOD_REQUEST) && $this->getAttendeeRSVP($attendee)) {
256
-
257
-			/*
73
+    /** @var string */
74
+    private $userId;
75
+
76
+    /** @var IConfig */
77
+    private $config;
78
+
79
+    /** @var IMailer */
80
+    private $mailer;
81
+
82
+    /** @var ILogger */
83
+    private $logger;
84
+
85
+    /** @var ITimeFactory */
86
+    private $timeFactory;
87
+
88
+    /** @var L10NFactory */
89
+    private $l10nFactory;
90
+
91
+    /** @var IURLGenerator */
92
+    private $urlGenerator;
93
+
94
+    /** @var ISecureRandom */
95
+    private $random;
96
+
97
+    /** @var IDBConnection */
98
+    private $db;
99
+
100
+    /** @var Defaults */
101
+    private $defaults;
102
+
103
+    /** @var IUserManager */
104
+    private $userManager;
105
+
106
+    public const MAX_DATE = '2038-01-01';
107
+
108
+    public const METHOD_REQUEST = 'request';
109
+    public const METHOD_REPLY = 'reply';
110
+    public const METHOD_CANCEL = 'cancel';
111
+    public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages
112
+
113
+    /**
114
+     * @param IConfig $config
115
+     * @param IMailer $mailer
116
+     * @param ILogger $logger
117
+     * @param ITimeFactory $timeFactory
118
+     * @param L10NFactory $l10nFactory
119
+     * @param IUrlGenerator $urlGenerator
120
+     * @param Defaults $defaults
121
+     * @param ISecureRandom $random
122
+     * @param IDBConnection $db
123
+     * @param string $userId
124
+     */
125
+    public function __construct(IConfig $config, IMailer $mailer, ILogger $logger,
126
+                                ITimeFactory $timeFactory, L10NFactory $l10nFactory,
127
+                                IURLGenerator $urlGenerator, Defaults $defaults,
128
+                                ISecureRandom $random, IDBConnection $db, IUserManager $userManager,
129
+                                $userId) {
130
+        parent::__construct('');
131
+        $this->userId = $userId;
132
+        $this->config = $config;
133
+        $this->mailer = $mailer;
134
+        $this->logger = $logger;
135
+        $this->timeFactory = $timeFactory;
136
+        $this->l10nFactory = $l10nFactory;
137
+        $this->urlGenerator = $urlGenerator;
138
+        $this->random = $random;
139
+        $this->db = $db;
140
+        $this->defaults = $defaults;
141
+        $this->userManager = $userManager;
142
+    }
143
+
144
+    /**
145
+     * Event handler for the 'schedule' event.
146
+     *
147
+     * @param Message $iTipMessage
148
+     * @return void
149
+     */
150
+    public function schedule(Message $iTipMessage) {
151
+
152
+        // Not sending any emails if the system considers the update
153
+        // insignificant.
154
+        if (!$iTipMessage->significantChange) {
155
+            if (!$iTipMessage->scheduleStatus) {
156
+                $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
157
+            }
158
+            return;
159
+        }
160
+
161
+        $summary = $iTipMessage->message->VEVENT->SUMMARY;
162
+
163
+        if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') {
164
+            return;
165
+        }
166
+
167
+        if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
168
+            return;
169
+        }
170
+
171
+        // don't send out mails for events that already took place
172
+        $lastOccurrence = $this->getLastOccurrence($iTipMessage->message);
173
+        $currentTime = $this->timeFactory->getTime();
174
+        if ($lastOccurrence < $currentTime) {
175
+            return;
176
+        }
177
+
178
+        // Strip off mailto:
179
+        $sender = substr($iTipMessage->sender, 7);
180
+        $recipient = substr($iTipMessage->recipient, 7);
181
+        if (!$this->mailer->validateMailAddress($recipient)) {
182
+            // Nothing to send if the recipient doesn't have a valid email address
183
+            $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
184
+            return;
185
+        }
186
+
187
+        $senderName = $iTipMessage->senderName ?: null;
188
+        $recipientName = $iTipMessage->recipientName ?: null;
189
+
190
+        if ($senderName === null || empty(trim($senderName))) {
191
+            $user = $this->userManager->get($this->userId);
192
+            if ($user) {
193
+                // getDisplayName automatically uses the uid
194
+                // if no display-name is set
195
+                $senderName = $user->getDisplayName();
196
+            }
197
+        }
198
+
199
+        /** @var VEvent $vevent */
200
+        $vevent = $iTipMessage->message->VEVENT;
201
+
202
+        $attendee = $this->getCurrentAttendee($iTipMessage);
203
+        $defaultLang = $this->l10nFactory->findLanguage();
204
+        $lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee);
205
+        $l10n = $this->l10nFactory->get('dav', $lang);
206
+
207
+        $meetingAttendeeName = $recipientName ?: $recipient;
208
+        $meetingInviteeName = $senderName ?: $sender;
209
+
210
+        $meetingTitle = $vevent->SUMMARY;
211
+        $meetingDescription = $vevent->DESCRIPTION;
212
+
213
+
214
+        $meetingUrl = $vevent->URL;
215
+        $meetingLocation = $vevent->LOCATION;
216
+
217
+        $defaultVal = '--';
218
+
219
+        $method = self::METHOD_REQUEST;
220
+        switch (strtolower($iTipMessage->method)) {
221
+            case self::METHOD_REPLY:
222
+                $method = self::METHOD_REPLY;
223
+                break;
224
+            case self::METHOD_CANCEL:
225
+                $method = self::METHOD_CANCEL;
226
+                break;
227
+        }
228
+
229
+        $data = [
230
+            'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
231
+            'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
232
+            'meeting_title' => (string)$meetingTitle ?: $defaultVal,
233
+            'meeting_description' => (string)$meetingDescription ?: $defaultVal,
234
+            'meeting_url' => (string)$meetingUrl ?: $defaultVal,
235
+        ];
236
+
237
+        $fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
238
+        $fromName = $l10n->t('%1$s via %2$s', [$senderName, $this->defaults->getName()]);
239
+
240
+        $message = $this->mailer->createMessage()
241
+            ->setFrom([$fromEMail => $fromName])
242
+            ->setReplyTo([$sender => $senderName])
243
+            ->setTo([$recipient => $recipientName]);
244
+
245
+        $template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
246
+        $template->addHeader();
247
+
248
+        $summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
249
+
250
+        $this->addSubjectAndHeading($template, $l10n, $method, $summary);
251
+        $this->addBulletList($template, $l10n, $vevent);
252
+
253
+
254
+        // Only add response buttons to invitation requests: Fix Issue #11230
255
+        if (($method == self::METHOD_REQUEST) && $this->getAttendeeRSVP($attendee)) {
256
+
257
+            /*
258 258
 			** Only offer invitation accept/reject buttons, which link back to the
259 259
 			** nextcloud server, to recipients who can access the nextcloud server via
260 260
 			** their internet/intranet.  Issue #12156
@@ -273,441 +273,441 @@  discard block
 block discarded – undo
273 273
 			** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
274 274
 			*/
275 275
 
276
-			$recipientDomain = substr(strrchr($recipient, "@"), 1);
277
-			$invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
278
-
279
-			if (strcmp('yes', $invitationLinkRecipients[0]) === 0
280
-				 || in_array(strtolower($recipient), $invitationLinkRecipients)
281
-				 || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
282
-				$this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence);
283
-			}
284
-		}
285
-
286
-		$template->addFooter();
287
-
288
-		$message->useTemplate($template);
289
-
290
-		$attachment = $this->mailer->createAttachment(
291
-			$iTipMessage->message->serialize(),
292
-			'event.ics',// TODO(leon): Make file name unique, e.g. add event id
293
-			'text/calendar; method=' . $iTipMessage->method
294
-		);
295
-		$message->attach($attachment);
296
-
297
-		try {
298
-			$failed = $this->mailer->send($message);
299
-			$iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
300
-			if ($failed) {
301
-				$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
302
-				$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
303
-			}
304
-		} catch (\Exception $ex) {
305
-			$this->logger->logException($ex, ['app' => 'dav']);
306
-			$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
307
-		}
308
-	}
309
-
310
-	/**
311
-	 * check if event took place in the past already
312
-	 * @param VCalendar $vObject
313
-	 * @return int
314
-	 */
315
-	private function getLastOccurrence(VCalendar $vObject) {
316
-		/** @var VEvent $component */
317
-		$component = $vObject->VEVENT;
318
-
319
-		$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
320
-		// Finding the last occurrence is a bit harder
321
-		if (!isset($component->RRULE)) {
322
-			if (isset($component->DTEND)) {
323
-				$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
324
-			} elseif (isset($component->DURATION)) {
325
-				/** @var \DateTime $endDate */
326
-				$endDate = clone $component->DTSTART->getDateTime();
327
-				// $component->DTEND->getDateTime() returns DateTimeImmutable
328
-				$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
329
-				$lastOccurrence = $endDate->getTimestamp();
330
-			} elseif (!$component->DTSTART->hasTime()) {
331
-				/** @var \DateTime $endDate */
332
-				$endDate = clone $component->DTSTART->getDateTime();
333
-				// $component->DTSTART->getDateTime() returns DateTimeImmutable
334
-				$endDate = $endDate->modify('+1 day');
335
-				$lastOccurrence = $endDate->getTimestamp();
336
-			} else {
337
-				$lastOccurrence = $firstOccurrence;
338
-			}
339
-		} else {
340
-			$it = new EventIterator($vObject, (string)$component->UID);
341
-			$maxDate = new \DateTime(self::MAX_DATE);
342
-			if ($it->isInfinite()) {
343
-				$lastOccurrence = $maxDate->getTimestamp();
344
-			} else {
345
-				$end = $it->getDtEnd();
346
-				while ($it->valid() && $end < $maxDate) {
347
-					$end = $it->getDtEnd();
348
-					$it->next();
349
-				}
350
-				$lastOccurrence = $end->getTimestamp();
351
-			}
352
-		}
353
-
354
-		return $lastOccurrence;
355
-	}
356
-
357
-	/**
358
-	 * @param Message $iTipMessage
359
-	 * @return null|Property
360
-	 */
361
-	private function getCurrentAttendee(Message $iTipMessage) {
362
-		/** @var VEvent $vevent */
363
-		$vevent = $iTipMessage->message->VEVENT;
364
-		$attendees = $vevent->select('ATTENDEE');
365
-		foreach ($attendees as $attendee) {
366
-			/** @var Property $attendee */
367
-			if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
368
-				return $attendee;
369
-			}
370
-		}
371
-		return null;
372
-	}
373
-
374
-	/**
375
-	 * @param string $default
376
-	 * @param Property|null $attendee
377
-	 * @return string
378
-	 */
379
-	private function getAttendeeLangOrDefault($default, Property $attendee = null) {
380
-		if ($attendee !== null) {
381
-			$lang = $attendee->offsetGet('LANGUAGE');
382
-			if ($lang instanceof Parameter) {
383
-				return $lang->getValue();
384
-			}
385
-		}
386
-		return $default;
387
-	}
388
-
389
-	/**
390
-	 * @param Property|null $attendee
391
-	 * @return bool
392
-	 */
393
-	private function getAttendeeRSVP(Property $attendee = null) {
394
-		if ($attendee !== null) {
395
-			$rsvp = $attendee->offsetGet('RSVP');
396
-			if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
397
-				return true;
398
-			}
399
-		}
400
-		// RFC 5545 3.2.17: default RSVP is false
401
-		return false;
402
-	}
403
-
404
-	/**
405
-	 * @param IL10N $l10n
406
-	 * @param VEvent $vevent
407
-	 */
408
-	private function generateWhenString(IL10N $l10n, VEvent $vevent) {
409
-		$dtstart = $vevent->DTSTART;
410
-		if (isset($vevent->DTEND)) {
411
-			$dtend = $vevent->DTEND;
412
-		} elseif (isset($vevent->DURATION)) {
413
-			$isFloating = $vevent->DTSTART->isFloating();
414
-			$dtend = clone $vevent->DTSTART;
415
-			$endDateTime = $dtend->getDateTime();
416
-			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
417
-			$dtend->setDateTime($endDateTime, $isFloating);
418
-		} elseif (!$vevent->DTSTART->hasTime()) {
419
-			$isFloating = $vevent->DTSTART->isFloating();
420
-			$dtend = clone $vevent->DTSTART;
421
-			$endDateTime = $dtend->getDateTime();
422
-			$endDateTime = $endDateTime->modify('+1 day');
423
-			$dtend->setDateTime($endDateTime, $isFloating);
424
-		} else {
425
-			$dtend = clone $vevent->DTSTART;
426
-		}
427
-
428
-		$isAllDay = $dtstart instanceof Property\ICalendar\Date;
429
-
430
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
431
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
432
-		/** @var \DateTimeImmutable $dtstartDt */
433
-		$dtstartDt = $dtstart->getDateTime();
434
-		/** @var \DateTimeImmutable $dtendDt */
435
-		$dtendDt = $dtend->getDateTime();
436
-
437
-		$diff = $dtstartDt->diff($dtendDt);
438
-
439
-		$dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
440
-		$dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
441
-
442
-		if ($isAllDay) {
443
-			// One day event
444
-			if ($diff->days === 1) {
445
-				return $l10n->l('date', $dtstartDt, ['width' => 'medium']);
446
-			}
447
-
448
-			// DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
449
-			// the email should show 2020-01-01 to 2020-01-04.
450
-			$dtendDt->modify('-1 day');
451
-
452
-			//event that spans over multiple days
453
-			$localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
454
-			$localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
455
-
456
-			return $localeStart . ' - ' . $localeEnd;
457
-		}
458
-
459
-		/** @var Property\ICalendar\DateTime $dtstart */
460
-		/** @var Property\ICalendar\DateTime $dtend */
461
-		$isFloating = $dtstart->isFloating();
462
-		$startTimezone = $endTimezone = null;
463
-		if (!$isFloating) {
464
-			$prop = $dtstart->offsetGet('TZID');
465
-			if ($prop instanceof Parameter) {
466
-				$startTimezone = $prop->getValue();
467
-			}
468
-
469
-			$prop = $dtend->offsetGet('TZID');
470
-			if ($prop instanceof Parameter) {
471
-				$endTimezone = $prop->getValue();
472
-			}
473
-		}
474
-
475
-		$localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
476
-			$l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
477
-
478
-		// always show full date with timezone if timezones are different
479
-		if ($startTimezone !== $endTimezone) {
480
-			$localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
481
-
482
-			return $localeStart . ' (' . $startTimezone . ') - ' .
483
-				$localeEnd . ' (' . $endTimezone . ')';
484
-		}
485
-
486
-		// show only end time if date is the same
487
-		if ($this->isDayEqual($dtstartDt, $dtendDt)) {
488
-			$localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
489
-		} else {
490
-			$localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
491
-				$l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
492
-		}
493
-
494
-		return  $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
495
-	}
496
-
497
-	/**
498
-	 * @param \DateTime $dtStart
499
-	 * @param \DateTime $dtEnd
500
-	 * @return bool
501
-	 */
502
-	private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) {
503
-		return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
504
-	}
505
-
506
-	/**
507
-	 * @param IEMailTemplate $template
508
-	 * @param IL10N $l10n
509
-	 * @param string $method
510
-	 * @param string $summary
511
-	 */
512
-	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
513
-										  $method, $summary) {
514
-		if ($method === self::METHOD_CANCEL) {
515
-			$template->setSubject('Canceled: ' . $summary);
516
-			$template->addHeading($l10n->t('Invitation canceled'));
517
-		} elseif ($method === self::METHOD_REPLY) {
518
-			$template->setSubject('Re: ' . $summary);
519
-			$template->addHeading($l10n->t('Invitation updated'));
520
-		} else {
521
-			$template->setSubject('Invitation: ' . $summary);
522
-			$template->addHeading($l10n->t('Invitation'));
523
-		}
524
-	}
525
-
526
-	/**
527
-	 * @param IEMailTemplate $template
528
-	 * @param IL10N $l10n
529
-	 * @param VEVENT $vevent
530
-	 */
531
-	private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) {
532
-		if ($vevent->SUMMARY) {
533
-			$template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'),
534
-				$this->getAbsoluteImagePath('caldav/title.svg'),'','',self::IMIP_INDENT);
535
-		}
536
-		$meetingWhen = $this->generateWhenString($l10n, $vevent);
537
-		if ($meetingWhen) {
538
-			$template->addBodyListItem($meetingWhen, $l10n->t('Time:'),
539
-				$this->getAbsoluteImagePath('caldav/time.svg'),'','',self::IMIP_INDENT);
540
-		}
541
-		if ($vevent->LOCATION) {
542
-			$template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'),
543
-				$this->getAbsoluteImagePath('caldav/location.svg'),'','',self::IMIP_INDENT);
544
-		}
545
-		if ($vevent->URL) {
546
-			$url = $vevent->URL->getValue();
547
-			$template->addBodyListItem(sprintf('<a href="%s">%s</a>',
548
-					htmlspecialchars($url),
549
-					htmlspecialchars($url)),
550
-				$l10n->t('Link:'),
551
-				$this->getAbsoluteImagePath('caldav/link.svg'),
552
-				$url,'',self::IMIP_INDENT);
553
-		}
554
-
555
-		$this->addAttendees($template, $l10n, $vevent);
556
-
557
-		/* Put description last, like an email body, since it can be arbitrarily long */
558
-		if ($vevent->DESCRIPTION) {
559
-			$template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'),
560
-				$this->getAbsoluteImagePath('caldav/description.svg'),'','',self::IMIP_INDENT);
561
-		}
562
-	}
563
-
564
-	/**
565
-	 * addAttendees: add organizer and attendee names/emails to iMip mail.
566
-	 *
567
-	 * Enable with DAV setting: invitation_list_attendees (default: no)
568
-	 *
569
-	 * The default is 'no', which matches old behavior, and is privacy preserving.
570
-	 *
571
-	 * To enable including attendees in invitation emails:
572
-	 *   % php occ config:app:set dav invitation_list_attendees --value yes
573
-	 *
574
-	 * @param IEMailTemplate $template
575
-	 * @param IL10N $l10n
576
-	 * @param Message $iTipMessage
577
-	 * @param int $lastOccurrence
578
-	 * @author brad2014 on github.com
579
-	 */
580
-
581
-	private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) {
582
-		if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
583
-			return;
584
-		}
585
-
586
-		if (isset($vevent->ORGANIZER)) {
587
-			/** @var Property\ICalendar\CalAddress $organizer */
588
-			$organizer = $vevent->ORGANIZER;
589
-			$organizerURI = $organizer->getNormalizedValue();
590
-			list($scheme,$organizerEmail) = explode(':',$organizerURI,2); # strip off scheme mailto:
591
-			/** @var string|null $organizerName */
592
-			$organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
593
-			$organizerHTML = sprintf('<a href="%s">%s</a>',
594
-				htmlspecialchars($organizerURI),
595
-				htmlspecialchars($organizerName ?: $organizerEmail));
596
-			$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
597
-			if (isset($organizer['PARTSTAT'])) {
598
-				/** @var Parameter $partstat */
599
-				$partstat = $organizer['PARTSTAT'];
600
-				if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
601
-					$organizerHTML .= ' ✔︎';
602
-					$organizerText .= ' ✔︎';
603
-				}
604
-			}
605
-			$template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'),
606
-				$this->getAbsoluteImagePath('caldav/organizer.svg'),
607
-				$organizerText,'',self::IMIP_INDENT);
608
-		}
609
-
610
-		$attendees = $vevent->select('ATTENDEE');
611
-		if (count($attendees) === 0) {
612
-			return;
613
-		}
614
-
615
-		$attendeesHTML = [];
616
-		$attendeesText = [];
617
-		foreach ($attendees as $attendee) {
618
-			$attendeeURI = $attendee->getNormalizedValue();
619
-			list($scheme,$attendeeEmail) = explode(':',$attendeeURI,2); # strip off scheme mailto:
620
-			$attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
621
-			$attendeeHTML = sprintf('<a href="%s">%s</a>',
622
-				htmlspecialchars($attendeeURI),
623
-				htmlspecialchars($attendeeName ?: $attendeeEmail));
624
-			$attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
625
-			if (isset($attendee['PARTSTAT'])
626
-				&& strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) {
627
-				$attendeeHTML .= ' ✔︎';
628
-				$attendeeText .= ' ✔︎';
629
-			}
630
-			array_push($attendeesHTML, $attendeeHTML);
631
-			array_push($attendeesText, $attendeeText);
632
-		}
633
-
634
-		$template->addBodyListItem(implode('<br/>',$attendeesHTML), $l10n->t('Attendees:'),
635
-			$this->getAbsoluteImagePath('caldav/attendees.svg'),
636
-			implode("\n",$attendeesText),'',self::IMIP_INDENT);
637
-	}
638
-
639
-	/**
640
-	 * @param IEMailTemplate $template
641
-	 * @param IL10N $l10n
642
-	 * @param Message $iTipMessage
643
-	 * @param int $lastOccurrence
644
-	 */
645
-	private function addResponseButtons(IEMailTemplate $template, IL10N $l10n,
646
-										Message $iTipMessage, $lastOccurrence) {
647
-		$token = $this->createInvitationToken($iTipMessage, $lastOccurrence);
648
-
649
-		$template->addBodyButtonGroup(
650
-			$l10n->t('Accept'),
651
-			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
652
-				'token' => $token,
653
-			]),
654
-			$l10n->t('Decline'),
655
-			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
656
-				'token' => $token,
657
-			])
658
-		);
659
-
660
-		$moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
661
-			'token' => $token,
662
-		]);
663
-		$html = vsprintf('<small><a href="%s">%s</a></small>', [
664
-			$moreOptionsURL, $l10n->t('More options …')
665
-		]);
666
-		$text = $l10n->t('More options at %s', [$moreOptionsURL]);
667
-
668
-		$template->addBodyText($html, $text);
669
-	}
670
-
671
-	/**
672
-	 * @param string $path
673
-	 * @return string
674
-	 */
675
-	private function getAbsoluteImagePath($path) {
676
-		return $this->urlGenerator->getAbsoluteURL(
677
-			$this->urlGenerator->imagePath('core', $path)
678
-		);
679
-	}
680
-
681
-	/**
682
-	 * @param Message $iTipMessage
683
-	 * @param int $lastOccurrence
684
-	 * @return string
685
-	 */
686
-	private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
687
-		$token = $this->random->generate(60, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
688
-
689
-		/** @var VEvent $vevent */
690
-		$vevent = $iTipMessage->message->VEVENT;
691
-		$attendee = $iTipMessage->recipient;
692
-		$organizer = $iTipMessage->sender;
693
-		$sequence = $iTipMessage->sequence;
694
-		$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
695
-			$vevent->{'RECURRENCE-ID'}->serialize() : null;
696
-		$uid = $vevent->{'UID'};
697
-
698
-		$query = $this->db->getQueryBuilder();
699
-		$query->insert('calendar_invitations')
700
-			->values([
701
-				'token' => $query->createNamedParameter($token),
702
-				'attendee' => $query->createNamedParameter($attendee),
703
-				'organizer' => $query->createNamedParameter($organizer),
704
-				'sequence' => $query->createNamedParameter($sequence),
705
-				'recurrenceid' => $query->createNamedParameter($recurrenceId),
706
-				'expiration' => $query->createNamedParameter($lastOccurrence),
707
-				'uid' => $query->createNamedParameter($uid)
708
-			])
709
-			->execute();
710
-
711
-		return $token;
712
-	}
276
+            $recipientDomain = substr(strrchr($recipient, "@"), 1);
277
+            $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
278
+
279
+            if (strcmp('yes', $invitationLinkRecipients[0]) === 0
280
+                 || in_array(strtolower($recipient), $invitationLinkRecipients)
281
+                 || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
282
+                $this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence);
283
+            }
284
+        }
285
+
286
+        $template->addFooter();
287
+
288
+        $message->useTemplate($template);
289
+
290
+        $attachment = $this->mailer->createAttachment(
291
+            $iTipMessage->message->serialize(),
292
+            'event.ics',// TODO(leon): Make file name unique, e.g. add event id
293
+            'text/calendar; method=' . $iTipMessage->method
294
+        );
295
+        $message->attach($attachment);
296
+
297
+        try {
298
+            $failed = $this->mailer->send($message);
299
+            $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
300
+            if ($failed) {
301
+                $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
302
+                $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
303
+            }
304
+        } catch (\Exception $ex) {
305
+            $this->logger->logException($ex, ['app' => 'dav']);
306
+            $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
307
+        }
308
+    }
309
+
310
+    /**
311
+     * check if event took place in the past already
312
+     * @param VCalendar $vObject
313
+     * @return int
314
+     */
315
+    private function getLastOccurrence(VCalendar $vObject) {
316
+        /** @var VEvent $component */
317
+        $component = $vObject->VEVENT;
318
+
319
+        $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
320
+        // Finding the last occurrence is a bit harder
321
+        if (!isset($component->RRULE)) {
322
+            if (isset($component->DTEND)) {
323
+                $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
324
+            } elseif (isset($component->DURATION)) {
325
+                /** @var \DateTime $endDate */
326
+                $endDate = clone $component->DTSTART->getDateTime();
327
+                // $component->DTEND->getDateTime() returns DateTimeImmutable
328
+                $endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
329
+                $lastOccurrence = $endDate->getTimestamp();
330
+            } elseif (!$component->DTSTART->hasTime()) {
331
+                /** @var \DateTime $endDate */
332
+                $endDate = clone $component->DTSTART->getDateTime();
333
+                // $component->DTSTART->getDateTime() returns DateTimeImmutable
334
+                $endDate = $endDate->modify('+1 day');
335
+                $lastOccurrence = $endDate->getTimestamp();
336
+            } else {
337
+                $lastOccurrence = $firstOccurrence;
338
+            }
339
+        } else {
340
+            $it = new EventIterator($vObject, (string)$component->UID);
341
+            $maxDate = new \DateTime(self::MAX_DATE);
342
+            if ($it->isInfinite()) {
343
+                $lastOccurrence = $maxDate->getTimestamp();
344
+            } else {
345
+                $end = $it->getDtEnd();
346
+                while ($it->valid() && $end < $maxDate) {
347
+                    $end = $it->getDtEnd();
348
+                    $it->next();
349
+                }
350
+                $lastOccurrence = $end->getTimestamp();
351
+            }
352
+        }
353
+
354
+        return $lastOccurrence;
355
+    }
356
+
357
+    /**
358
+     * @param Message $iTipMessage
359
+     * @return null|Property
360
+     */
361
+    private function getCurrentAttendee(Message $iTipMessage) {
362
+        /** @var VEvent $vevent */
363
+        $vevent = $iTipMessage->message->VEVENT;
364
+        $attendees = $vevent->select('ATTENDEE');
365
+        foreach ($attendees as $attendee) {
366
+            /** @var Property $attendee */
367
+            if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
368
+                return $attendee;
369
+            }
370
+        }
371
+        return null;
372
+    }
373
+
374
+    /**
375
+     * @param string $default
376
+     * @param Property|null $attendee
377
+     * @return string
378
+     */
379
+    private function getAttendeeLangOrDefault($default, Property $attendee = null) {
380
+        if ($attendee !== null) {
381
+            $lang = $attendee->offsetGet('LANGUAGE');
382
+            if ($lang instanceof Parameter) {
383
+                return $lang->getValue();
384
+            }
385
+        }
386
+        return $default;
387
+    }
388
+
389
+    /**
390
+     * @param Property|null $attendee
391
+     * @return bool
392
+     */
393
+    private function getAttendeeRSVP(Property $attendee = null) {
394
+        if ($attendee !== null) {
395
+            $rsvp = $attendee->offsetGet('RSVP');
396
+            if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
397
+                return true;
398
+            }
399
+        }
400
+        // RFC 5545 3.2.17: default RSVP is false
401
+        return false;
402
+    }
403
+
404
+    /**
405
+     * @param IL10N $l10n
406
+     * @param VEvent $vevent
407
+     */
408
+    private function generateWhenString(IL10N $l10n, VEvent $vevent) {
409
+        $dtstart = $vevent->DTSTART;
410
+        if (isset($vevent->DTEND)) {
411
+            $dtend = $vevent->DTEND;
412
+        } elseif (isset($vevent->DURATION)) {
413
+            $isFloating = $vevent->DTSTART->isFloating();
414
+            $dtend = clone $vevent->DTSTART;
415
+            $endDateTime = $dtend->getDateTime();
416
+            $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
417
+            $dtend->setDateTime($endDateTime, $isFloating);
418
+        } elseif (!$vevent->DTSTART->hasTime()) {
419
+            $isFloating = $vevent->DTSTART->isFloating();
420
+            $dtend = clone $vevent->DTSTART;
421
+            $endDateTime = $dtend->getDateTime();
422
+            $endDateTime = $endDateTime->modify('+1 day');
423
+            $dtend->setDateTime($endDateTime, $isFloating);
424
+        } else {
425
+            $dtend = clone $vevent->DTSTART;
426
+        }
427
+
428
+        $isAllDay = $dtstart instanceof Property\ICalendar\Date;
429
+
430
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
431
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
432
+        /** @var \DateTimeImmutable $dtstartDt */
433
+        $dtstartDt = $dtstart->getDateTime();
434
+        /** @var \DateTimeImmutable $dtendDt */
435
+        $dtendDt = $dtend->getDateTime();
436
+
437
+        $diff = $dtstartDt->diff($dtendDt);
438
+
439
+        $dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
440
+        $dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
441
+
442
+        if ($isAllDay) {
443
+            // One day event
444
+            if ($diff->days === 1) {
445
+                return $l10n->l('date', $dtstartDt, ['width' => 'medium']);
446
+            }
447
+
448
+            // DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
449
+            // the email should show 2020-01-01 to 2020-01-04.
450
+            $dtendDt->modify('-1 day');
451
+
452
+            //event that spans over multiple days
453
+            $localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
454
+            $localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
455
+
456
+            return $localeStart . ' - ' . $localeEnd;
457
+        }
458
+
459
+        /** @var Property\ICalendar\DateTime $dtstart */
460
+        /** @var Property\ICalendar\DateTime $dtend */
461
+        $isFloating = $dtstart->isFloating();
462
+        $startTimezone = $endTimezone = null;
463
+        if (!$isFloating) {
464
+            $prop = $dtstart->offsetGet('TZID');
465
+            if ($prop instanceof Parameter) {
466
+                $startTimezone = $prop->getValue();
467
+            }
468
+
469
+            $prop = $dtend->offsetGet('TZID');
470
+            if ($prop instanceof Parameter) {
471
+                $endTimezone = $prop->getValue();
472
+            }
473
+        }
474
+
475
+        $localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
476
+            $l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
477
+
478
+        // always show full date with timezone if timezones are different
479
+        if ($startTimezone !== $endTimezone) {
480
+            $localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
481
+
482
+            return $localeStart . ' (' . $startTimezone . ') - ' .
483
+                $localeEnd . ' (' . $endTimezone . ')';
484
+        }
485
+
486
+        // show only end time if date is the same
487
+        if ($this->isDayEqual($dtstartDt, $dtendDt)) {
488
+            $localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
489
+        } else {
490
+            $localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
491
+                $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
492
+        }
493
+
494
+        return  $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
495
+    }
496
+
497
+    /**
498
+     * @param \DateTime $dtStart
499
+     * @param \DateTime $dtEnd
500
+     * @return bool
501
+     */
502
+    private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) {
503
+        return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
504
+    }
505
+
506
+    /**
507
+     * @param IEMailTemplate $template
508
+     * @param IL10N $l10n
509
+     * @param string $method
510
+     * @param string $summary
511
+     */
512
+    private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
513
+                                            $method, $summary) {
514
+        if ($method === self::METHOD_CANCEL) {
515
+            $template->setSubject('Canceled: ' . $summary);
516
+            $template->addHeading($l10n->t('Invitation canceled'));
517
+        } elseif ($method === self::METHOD_REPLY) {
518
+            $template->setSubject('Re: ' . $summary);
519
+            $template->addHeading($l10n->t('Invitation updated'));
520
+        } else {
521
+            $template->setSubject('Invitation: ' . $summary);
522
+            $template->addHeading($l10n->t('Invitation'));
523
+        }
524
+    }
525
+
526
+    /**
527
+     * @param IEMailTemplate $template
528
+     * @param IL10N $l10n
529
+     * @param VEVENT $vevent
530
+     */
531
+    private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) {
532
+        if ($vevent->SUMMARY) {
533
+            $template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'),
534
+                $this->getAbsoluteImagePath('caldav/title.svg'),'','',self::IMIP_INDENT);
535
+        }
536
+        $meetingWhen = $this->generateWhenString($l10n, $vevent);
537
+        if ($meetingWhen) {
538
+            $template->addBodyListItem($meetingWhen, $l10n->t('Time:'),
539
+                $this->getAbsoluteImagePath('caldav/time.svg'),'','',self::IMIP_INDENT);
540
+        }
541
+        if ($vevent->LOCATION) {
542
+            $template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'),
543
+                $this->getAbsoluteImagePath('caldav/location.svg'),'','',self::IMIP_INDENT);
544
+        }
545
+        if ($vevent->URL) {
546
+            $url = $vevent->URL->getValue();
547
+            $template->addBodyListItem(sprintf('<a href="%s">%s</a>',
548
+                    htmlspecialchars($url),
549
+                    htmlspecialchars($url)),
550
+                $l10n->t('Link:'),
551
+                $this->getAbsoluteImagePath('caldav/link.svg'),
552
+                $url,'',self::IMIP_INDENT);
553
+        }
554
+
555
+        $this->addAttendees($template, $l10n, $vevent);
556
+
557
+        /* Put description last, like an email body, since it can be arbitrarily long */
558
+        if ($vevent->DESCRIPTION) {
559
+            $template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'),
560
+                $this->getAbsoluteImagePath('caldav/description.svg'),'','',self::IMIP_INDENT);
561
+        }
562
+    }
563
+
564
+    /**
565
+     * addAttendees: add organizer and attendee names/emails to iMip mail.
566
+     *
567
+     * Enable with DAV setting: invitation_list_attendees (default: no)
568
+     *
569
+     * The default is 'no', which matches old behavior, and is privacy preserving.
570
+     *
571
+     * To enable including attendees in invitation emails:
572
+     *   % php occ config:app:set dav invitation_list_attendees --value yes
573
+     *
574
+     * @param IEMailTemplate $template
575
+     * @param IL10N $l10n
576
+     * @param Message $iTipMessage
577
+     * @param int $lastOccurrence
578
+     * @author brad2014 on github.com
579
+     */
580
+
581
+    private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) {
582
+        if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
583
+            return;
584
+        }
585
+
586
+        if (isset($vevent->ORGANIZER)) {
587
+            /** @var Property\ICalendar\CalAddress $organizer */
588
+            $organizer = $vevent->ORGANIZER;
589
+            $organizerURI = $organizer->getNormalizedValue();
590
+            list($scheme,$organizerEmail) = explode(':',$organizerURI,2); # strip off scheme mailto:
591
+            /** @var string|null $organizerName */
592
+            $organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
593
+            $organizerHTML = sprintf('<a href="%s">%s</a>',
594
+                htmlspecialchars($organizerURI),
595
+                htmlspecialchars($organizerName ?: $organizerEmail));
596
+            $organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
597
+            if (isset($organizer['PARTSTAT'])) {
598
+                /** @var Parameter $partstat */
599
+                $partstat = $organizer['PARTSTAT'];
600
+                if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
601
+                    $organizerHTML .= ' ✔︎';
602
+                    $organizerText .= ' ✔︎';
603
+                }
604
+            }
605
+            $template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'),
606
+                $this->getAbsoluteImagePath('caldav/organizer.svg'),
607
+                $organizerText,'',self::IMIP_INDENT);
608
+        }
609
+
610
+        $attendees = $vevent->select('ATTENDEE');
611
+        if (count($attendees) === 0) {
612
+            return;
613
+        }
614
+
615
+        $attendeesHTML = [];
616
+        $attendeesText = [];
617
+        foreach ($attendees as $attendee) {
618
+            $attendeeURI = $attendee->getNormalizedValue();
619
+            list($scheme,$attendeeEmail) = explode(':',$attendeeURI,2); # strip off scheme mailto:
620
+            $attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
621
+            $attendeeHTML = sprintf('<a href="%s">%s</a>',
622
+                htmlspecialchars($attendeeURI),
623
+                htmlspecialchars($attendeeName ?: $attendeeEmail));
624
+            $attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
625
+            if (isset($attendee['PARTSTAT'])
626
+                && strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) {
627
+                $attendeeHTML .= ' ✔︎';
628
+                $attendeeText .= ' ✔︎';
629
+            }
630
+            array_push($attendeesHTML, $attendeeHTML);
631
+            array_push($attendeesText, $attendeeText);
632
+        }
633
+
634
+        $template->addBodyListItem(implode('<br/>',$attendeesHTML), $l10n->t('Attendees:'),
635
+            $this->getAbsoluteImagePath('caldav/attendees.svg'),
636
+            implode("\n",$attendeesText),'',self::IMIP_INDENT);
637
+    }
638
+
639
+    /**
640
+     * @param IEMailTemplate $template
641
+     * @param IL10N $l10n
642
+     * @param Message $iTipMessage
643
+     * @param int $lastOccurrence
644
+     */
645
+    private function addResponseButtons(IEMailTemplate $template, IL10N $l10n,
646
+                                        Message $iTipMessage, $lastOccurrence) {
647
+        $token = $this->createInvitationToken($iTipMessage, $lastOccurrence);
648
+
649
+        $template->addBodyButtonGroup(
650
+            $l10n->t('Accept'),
651
+            $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
652
+                'token' => $token,
653
+            ]),
654
+            $l10n->t('Decline'),
655
+            $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
656
+                'token' => $token,
657
+            ])
658
+        );
659
+
660
+        $moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
661
+            'token' => $token,
662
+        ]);
663
+        $html = vsprintf('<small><a href="%s">%s</a></small>', [
664
+            $moreOptionsURL, $l10n->t('More options …')
665
+        ]);
666
+        $text = $l10n->t('More options at %s', [$moreOptionsURL]);
667
+
668
+        $template->addBodyText($html, $text);
669
+    }
670
+
671
+    /**
672
+     * @param string $path
673
+     * @return string
674
+     */
675
+    private function getAbsoluteImagePath($path) {
676
+        return $this->urlGenerator->getAbsoluteURL(
677
+            $this->urlGenerator->imagePath('core', $path)
678
+        );
679
+    }
680
+
681
+    /**
682
+     * @param Message $iTipMessage
683
+     * @param int $lastOccurrence
684
+     * @return string
685
+     */
686
+    private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
687
+        $token = $this->random->generate(60, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
688
+
689
+        /** @var VEvent $vevent */
690
+        $vevent = $iTipMessage->message->VEVENT;
691
+        $attendee = $iTipMessage->recipient;
692
+        $organizer = $iTipMessage->sender;
693
+        $sequence = $iTipMessage->sequence;
694
+        $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
695
+            $vevent->{'RECURRENCE-ID'}->serialize() : null;
696
+        $uid = $vevent->{'UID'};
697
+
698
+        $query = $this->db->getQueryBuilder();
699
+        $query->insert('calendar_invitations')
700
+            ->values([
701
+                'token' => $query->createNamedParameter($token),
702
+                'attendee' => $query->createNamedParameter($attendee),
703
+                'organizer' => $query->createNamedParameter($organizer),
704
+                'sequence' => $query->createNamedParameter($sequence),
705
+                'recurrenceid' => $query->createNamedParameter($recurrenceId),
706
+                'expiration' => $query->createNamedParameter($lastOccurrence),
707
+                'uid' => $query->createNamedParameter($uid)
708
+            ])
709
+            ->execute();
710
+
711
+        return $token;
712
+    }
713 713
 }
Please login to merge, or discard this patch.