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