Passed
Push — master ( 920174...784b2b )
by Blizzz
16:18 queued 14s
created

IMipService   F

Complexity

Total Complexity 97

Size/Duplication

Total Lines 564
Duplicated Lines 0 %

Importance

Changes 2
Bugs 2 Features 1
Metric Value
eloc 279
dl 0
loc 564
rs 2
c 2
b 2
f 1
wmc 97

19 Methods

Rating   Name   Duplication   Size   Complexity  
B generateWhenString() 0 88 11
C addAttendees() 0 57 13
A setL10n() 0 9 3
A getCurrentAttendee() 0 11 3
A addBulletList() 0 23 5
B getLastOccurrence() 0 46 8
A __construct() 0 12 1
A getFrom() 0 6 2
F buildCancelledBodyData() 0 22 15
A generateVCalendar() 0 11 3
A addMoreOptionsButton() 0 10 1
A addSubjectAndHeading() 0 18 5
A addResponseButtons() 0 9 1
A readPropertyWithDefault() 0 8 3
A getAbsoluteImagePath() 0 3 1
B getAttendeeRsvpOrReqForParticipant() 0 21 9
A createInvitationToken() 0 24 2
B buildBodyData() 0 26 7
A generateDiffString() 0 11 4

How to fix   Complexity   

Complex Class

Complex classes like IMipService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use IMipService, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
/*
4
 * DAV App
5
 *
6
 * @copyright 2022 Anna Larch <[email protected]>
7
 *
8
 * @author Anna Larch <[email protected]>
9
 *
10
 * This library is free software; you can redistribute it and/or
11
 * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
12
 * License as published by the Free Software Foundation; either
13
 * version 3 of the License, or any later version.
14
 *
15
 * This library is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public
21
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23
24
namespace OCA\DAV\CalDAV\Schedule;
25
26
use OC\URLGenerator;
27
use OCP\IConfig;
28
use OCP\IDBConnection;
29
use OCP\IL10N;
30
use OCP\L10N\IFactory as L10NFactory;
31
use OCP\Mail\IEMailTemplate;
32
use OCP\Security\ISecureRandom;
33
use Sabre\VObject\Component\VCalendar;
34
use Sabre\VObject\Component\VEvent;
35
use Sabre\VObject\DateTimeParser;
36
use Sabre\VObject\ITip\Message;
37
use Sabre\VObject\Parameter;
38
use Sabre\VObject\Property;
39
use Sabre\VObject\Recur\EventIterator;
40
41
class IMipService {
42
43
	private URLGenerator $urlGenerator;
44
	private IConfig $config;
45
	private IDBConnection $db;
46
	private ISecureRandom $random;
47
	private L10NFactory $l10nFactory;
48
	private IL10N $l10n;
49
50
	/** @var string[] */
51
	private const STRING_DIFF = [
52
		'meeting_title' => 'SUMMARY',
53
		'meeting_description' => 'DESCRIPTION',
54
		'meeting_url' => 'URL',
55
		'meeting_location' => 'LOCATION'
56
	];
57
58
	public function __construct(URLGenerator $urlGenerator,
59
								IConfig $config,
60
								IDBConnection $db,
61
								ISecureRandom $random,
62
								L10NFactory $l10nFactory) {
63
		$this->urlGenerator = $urlGenerator;
64
		$this->config = $config;
65
		$this->db = $db;
66
		$this->random = $random;
67
		$this->l10nFactory = $l10nFactory;
68
		$default = $this->l10nFactory->findGenericLanguage();
69
		$this->l10n = $this->l10nFactory->get('dav', $default);
70
	}
71
72
	/**
73
	 * @param string|null $senderName
74
	 * @param string $default
75
	 * @return string
76
	 */
77
	public function getFrom(?string $senderName, string $default): string {
78
		if ($senderName === null) {
79
			return $default;
80
		}
81
82
		return $this->l10n->t('%1$s via %2$s', [$senderName, $default]);
83
	}
84
85
	public static function readPropertyWithDefault(VEvent $vevent, string $property, string $default) {
86
		if (isset($vevent->$property)) {
87
			$value = $vevent->$property->getValue();
88
			if (!empty($value)) {
89
				return $value;
90
			}
91
		}
92
		return $default;
93
	}
94
95
	private function generateDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
96
		$strikethrough = "<span style='text-decoration: line-through'>%s</span><br />%s";
97
		if (!isset($vevent->$property)) {
98
			return $default;
99
		}
100
		$newstring = $vevent->$property->getValue();
101
		if(isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring ) {
102
			$oldstring = $oldVEvent->$property->getValue();
103
			return sprintf($strikethrough, $oldstring, $newstring);
104
		}
105
		return $newstring;
106
	}
107
108
	/**
109
	 * @param VEvent $vEvent
110
	 * @param VEvent|null $oldVEvent
111
	 * @return array
112
	 */
113
	public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array {
114
		$defaultVal = '';
115
		$data = [];
116
		$data['meeting_when'] = $this->generateWhenString($vEvent);
117
118
		foreach(self::STRING_DIFF as $key => $property) {
119
			$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
120
		}
121
122
		$data['meeting_url_html'] = self::readPropertyWithDefault($vEvent, 'URL', $defaultVal);
123
124
		if(!empty($oldVEvent)) {
125
			$oldMeetingWhen = $this->generateWhenString($oldVEvent);
126
			$data['meeting_title_html']	= $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
127
			$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
128
			$data['meeting_location_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
129
130
			$oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
131
			$data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url'];
132
133
			$data['meeting_when_html'] =
134
				($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null)
135
					? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when'])
136
					: $data['meeting_when'];
137
		}
138
		return $data;
139
	}
140
141
	/**
142
	 * @param IL10N $this->l10n
143
	 * @param VEvent $vevent
144
	 * @return false|int|string
145
	 */
146
	public function generateWhenString(VEvent $vevent) {
147
		/** @var Property\ICalendar\DateTime $dtstart */
148
		$dtstart = $vevent->DTSTART;
149
		if (isset($vevent->DTEND)) {
150
			/** @var Property\ICalendar\DateTime $dtend */
151
			$dtend = $vevent->DTEND;
152
		} elseif (isset($vevent->DURATION)) {
153
			$isFloating = $dtstart->isFloating();
154
			$dtend = clone $dtstart;
155
			$endDateTime = $dtend->getDateTime();
156
			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

156
			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->/** @scrutinizer ignore-call */ getValue()));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
157
			$dtend->setDateTime($endDateTime, $isFloating);
158
		} elseif (!$dtstart->hasTime()) {
159
			$isFloating = $dtstart->isFloating();
160
			$dtend = clone $dtstart;
161
			$endDateTime = $dtend->getDateTime();
162
			$endDateTime = $endDateTime->modify('+1 day');
163
			$dtend->setDateTime($endDateTime, $isFloating);
164
		} else {
165
			$dtend = clone $dtstart;
166
		}
167
168
		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
169
		/** @var \DateTimeImmutable $dtstartDt */
170
		$dtstartDt = $dtstart->getDateTime();
171
172
		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
173
		/** @var \DateTimeImmutable $dtendDt */
174
		$dtendDt = $dtend->getDateTime();
175
176
		$diff = $dtstartDt->diff($dtendDt);
177
178
		$dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
179
		$dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
180
181
		if ($dtstart instanceof Property\ICalendar\Date) {
182
			// One day event
183
			if ($diff->days === 1) {
184
				return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
185
			}
186
187
			// DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
188
			// the email should show 2020-01-01 to 2020-01-04.
189
			$dtendDt->modify('-1 day');
190
191
			//event that spans over multiple days
192
			$localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
193
			$localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']);
194
195
			return $localeStart . ' - ' . $localeEnd;
0 ignored issues
show
Bug introduced by
Are you sure $localeStart of type false|integer|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

195
			return /** @scrutinizer ignore-type */ $localeStart . ' - ' . $localeEnd;
Loading history...
Bug introduced by
Are you sure $localeEnd of type false|integer|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

195
			return $localeStart . ' - ' . /** @scrutinizer ignore-type */ $localeEnd;
Loading history...
196
		}
197
198
		/** @var Property\ICalendar\DateTime $dtstart */
199
		/** @var Property\ICalendar\DateTime $dtend */
200
		$isFloating = $dtstart->isFloating();
201
		$startTimezone = $endTimezone = null;
202
		if (!$isFloating) {
203
			$prop = $dtstart->offsetGet('TZID');
204
			if ($prop instanceof Parameter) {
205
				$startTimezone = $prop->getValue();
206
			}
207
208
			$prop = $dtend->offsetGet('TZID');
209
			if ($prop instanceof Parameter) {
210
				$endTimezone = $prop->getValue();
211
			}
212
		}
213
214
		$localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
0 ignored issues
show
Bug introduced by
Are you sure $this->l10n->l('weekdayN...dth' => 'abbreviated')) of type false|integer|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

214
		$localeStart = /** @scrutinizer ignore-type */ $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
Loading history...
215
			$this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
0 ignored issues
show
Bug introduced by
Are you sure $this->l10n->l('datetime...th' => 'medium|short')) of type false|integer|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

215
			/** @scrutinizer ignore-type */ $this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
Loading history...
216
217
		// always show full date with timezone if timezones are different
218
		if ($startTimezone !== $endTimezone) {
219
			$localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
220
221
			return $localeStart . ' (' . $startTimezone . ') - ' .
222
				$localeEnd . ' (' . $endTimezone . ')';
223
		}
224
225
		// show only end time if date is the same
226
		if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) {
227
			$localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']);
228
		} else {
229
			$localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
0 ignored issues
show
Bug introduced by
Are you sure $this->l10n->l('weekdayN...dth' => 'abbreviated')) of type false|integer|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

229
			$localeEnd = /** @scrutinizer ignore-type */ $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
Loading history...
230
				$this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
0 ignored issues
show
Bug introduced by
Are you sure $this->l10n->l('datetime...th' => 'medium|short')) of type false|integer|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

230
				/** @scrutinizer ignore-type */ $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
Loading history...
231
		}
232
233
		return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
234
	}
235
236
	/**
237
	 * @param VEvent $vEvent
238
	 * @return array
239
	 */
240
	public function buildCancelledBodyData(VEvent $vEvent): array {
241
		$defaultVal = '';
242
		$strikethrough = "<span style='text-decoration: line-through'>%s</span>";
243
244
		$newMeetingWhen = $this->generateWhenString($vEvent);
245
		$newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');;
246
		$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
247
		$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
248
		$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
249
250
		$data = [];
251
		$data['meeting_when_html'] = $newMeetingWhen === '' ?: sprintf($strikethrough, $newMeetingWhen);
252
		$data['meeting_when'] = $newMeetingWhen;
253
		$data['meeting_title_html'] = sprintf($strikethrough, $newSummary);
254
		$data['meeting_title'] = $newSummary !== '' ? $newSummary: $this->l10n->t('Untitled event');
255
		$data['meeting_description_html'] = $newDescription !== '' ? sprintf($strikethrough, $newDescription) : '';
256
		$data['meeting_description'] = $newDescription;
257
		$data['meeting_url_html'] = $newUrl !== '' ? sprintf($strikethrough, $newUrl) : '';
258
		$data['meeting_url'] = isset($vEvent->URL) ? (string)$vEvent->URL : '';
259
		$data['meeting_location_html'] = $newLocation !== '' ? sprintf($strikethrough, $newLocation) : '';
260
		$data['meeting_location'] = $newLocation;
261
		return $data;
262
	}
263
264
	/**
265
	 * Check if event took place in the past
266
	 *
267
	 * @param VCalendar $vObject
268
	 * @return int
269
	 */
270
	public function getLastOccurrence(VCalendar $vObject) {
271
		/** @var VEvent $component */
272
		$component = $vObject->VEVENT;
273
274
		if (isset($component->RRULE)) {
275
			$it = new EventIterator($vObject, (string)$component->UID);
276
			$maxDate = new \DateTime(IMipPlugin::MAX_DATE);
277
			if ($it->isInfinite()) {
278
				return $maxDate->getTimestamp();
279
			}
280
281
			$end = $it->getDtEnd();
282
			while ($it->valid() && $end < $maxDate) {
283
				$end = $it->getDtEnd();
284
				$it->next();
285
			}
286
			return $end->getTimestamp();
287
		}
288
289
		/** @var Property\ICalendar\DateTime $dtStart */
290
		$dtStart = $component->DTSTART;
291
292
		if (isset($component->DTEND)) {
293
			/** @var Property\ICalendar\DateTime $dtEnd */
294
			$dtEnd = $component->DTEND;
295
			return $dtEnd->getDateTime()->getTimeStamp();
296
		}
297
298
		if(isset($component->DURATION)) {
299
			/** @var \DateTime $endDate */
300
			$endDate = clone $dtStart->getDateTime();
301
			// $component->DTEND->getDateTime() returns DateTimeImmutable
302
			$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

302
			$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->/** @scrutinizer ignore-call */ getValue()));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
303
			return $endDate->getTimestamp();
304
		}
305
306
		if(!$dtStart->hasTime()) {
307
			/** @var \DateTime $endDate */
308
			// $component->DTSTART->getDateTime() returns DateTimeImmutable
309
			$endDate = clone $dtStart->getDateTime();
310
			$endDate = $endDate->modify('+1 day');
311
			return $endDate->getTimestamp();
312
		}
313
314
		// No computation of end time possible - return start date
315
		return $dtStart->getDateTime()->getTimeStamp();
316
	}
317
318
	/**
319
	 * @param Property|null $attendee
320
	 */
321
	public function setL10n(?Property $attendee = null) {
322
		if($attendee === null) {
323
			return;
324
		}
325
326
		$lang = $attendee->offsetGet('LANGUAGE');
327
		if ($lang instanceof Parameter) {
328
			$lang = $lang->getValue();
329
			$this->l10n = $this->l10nFactory->get('dav', $lang);
330
		}
331
	}
332
333
	/**
334
	 * @param Property|null $attendee
335
	 * @return bool
336
	 */
337
	public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) {
338
		if($attendee === null) {
339
			return false;
340
		}
341
342
		$rsvp = $attendee->offsetGet('RSVP');
343
		if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
344
			return true;
345
		}
346
		$role = $attendee->offsetGet('ROLE');
347
		// @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16
348
		// Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set
349
		if ($role === null
350
			|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0))
351
			|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0))
352
		) {
353
			return true;
354
		}
355
356
		// RFC 5545 3.2.17: default RSVP is false
357
		return false;
358
	}
359
360
	/**
361
	 * @param IEMailTemplate $template
362
	 * @param string $method
363
	 * @param string $sender
364
	 * @param string $summary
365
	 * @param string|null $partstat
366
	 * @param bool $isModified
367
	 */
368
	public function addSubjectAndHeading(IEMailTemplate $template,
369
		string $method, string $sender, string $summary, bool $isModified): void {
370
		if ($method === IMipPlugin::METHOD_CANCEL) {
371
			// TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}"
372
			$template->setSubject($this->l10n->t('Cancelled: %1$s', [$summary]));
373
			$template->addHeading($this->l10n->t('"%1$s" has been canceled', [$summary]));
374
		} elseif ($method === IMipPlugin::METHOD_REPLY) {
375
			// TRANSLATORS Subject for email, when an invitation is replied to. Ex: "Re: {{Event Name}}"
376
			$template->setSubject($this->l10n->t('Re: %1$s', [$summary]));
377
			$template->addHeading($this->l10n->t('%1$s has responded to your invitation', [$sender]));
378
		} elseif ($method === IMipPlugin::METHOD_REQUEST && $isModified) {
379
			// TRANSLATORS Subject for email, when an invitation is updated. Ex: "Invitation updated: {{Event Name}}"
380
			$template->setSubject($this->l10n->t('Invitation updated: %1$s', [$summary]));
381
			$template->addHeading($this->l10n->t('%1$s updated the event "%2$s"', [$sender, $summary]));
382
		} else {
383
			// TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}"
384
			$template->setSubject($this->l10n->t('Invitation: %1$s', [$summary]));
385
			$template->addHeading($this->l10n->t('%1$s would like to invite you to "%2$s"', [$sender, $summary]));
386
		}
387
	}
388
389
	/**
390
	 * @param string $path
391
	 * @return string
392
	 */
393
	public function getAbsoluteImagePath($path): string {
394
		return $this->urlGenerator->getAbsoluteURL(
395
			$this->urlGenerator->imagePath('core', $path)
396
		);
397
	}
398
399
	/**
400
	 * addAttendees: add organizer and attendee names/emails to iMip mail.
401
	 *
402
	 * Enable with DAV setting: invitation_list_attendees (default: no)
403
	 *
404
	 * The default is 'no', which matches old behavior, and is privacy preserving.
405
	 *
406
	 * To enable including attendees in invitation emails:
407
	 *   % php occ config:app:set dav invitation_list_attendees --value yes
408
	 *
409
	 * @param IEMailTemplate $template
410
	 * @param IL10N $this->l10n
411
	 * @param VEvent $vevent
412
	 * @author brad2014 on github.com
413
	 */
414
	public function addAttendees(IEMailTemplate $template, VEvent $vevent) {
415
		if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
416
			return;
417
		}
418
419
		if (isset($vevent->ORGANIZER)) {
420
			/** @var Property | Property\ICalendar\CalAddress $organizer */
421
			$organizer = $vevent->ORGANIZER;
422
			$organizerEmail = substr($organizer->getNormalizedValue(), 7);
0 ignored issues
show
Bug introduced by
The method getNormalizedValue() does not exist on Sabre\VObject\Property. It seems like you code against a sub-type of Sabre\VObject\Property such as Sabre\VObject\Property\ICalendar\CalAddress. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

422
			$organizerEmail = substr($organizer->/** @scrutinizer ignore-call */ getNormalizedValue(), 7);
Loading history...
423
			/** @var string|null $organizerName */
424
			$organizerName = isset($organizer->CN) ? $organizer->CN->getValue() : null;
425
			$organizerHTML = sprintf('<a href="%s">%s</a>',
426
				htmlspecialchars($organizer->getNormalizedValue()),
427
				htmlspecialchars($organizerName ?: $organizerEmail));
428
			$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
429
			if(isset($organizer['PARTSTAT']) ) {
430
				/** @var Parameter $partstat */
431
				$partstat = $organizer['PARTSTAT'];
432
				if(strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
433
					$organizerHTML .= ' ✔︎';
434
					$organizerText .= ' ✔︎';
435
				}
436
			}
437
			$template->addBodyListItem($organizerHTML, $this->l10n->t('Organizer:'),
438
				$this->getAbsoluteImagePath('caldav/organizer.png'),
439
				$organizerText, '', IMipPlugin::IMIP_INDENT);
440
		}
441
442
		$attendees = $vevent->select('ATTENDEE');
443
		if (count($attendees) === 0) {
444
			return;
445
		}
446
447
		$attendeesHTML = [];
448
		$attendeesText = [];
449
		foreach ($attendees as $attendee) {
450
			$attendeeEmail = substr($attendee->getNormalizedValue(), 7);
451
			$attendeeName = isset($attendee['CN']) ? $attendee['CN']->getValue() : null;
452
			$attendeeHTML = sprintf('<a href="%s">%s</a>',
453
				htmlspecialchars($attendee->getNormalizedValue()),
454
				htmlspecialchars($attendeeName ?: $attendeeEmail));
455
			$attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
456
			if (isset($attendee['PARTSTAT'])) {
457
				/** @var Parameter $partstat */
458
				$partstat = $attendee['PARTSTAT'];
459
				if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
460
					$attendeeHTML .= ' ✔︎';
461
					$attendeeText .= ' ✔︎';
462
				}
463
			}
464
			$attendeesHTML[] = $attendeeHTML;
465
			$attendeesText[] = $attendeeText;
466
		}
467
468
		$template->addBodyListItem(implode('<br/>', $attendeesHTML), $this->l10n->t('Attendees:'),
469
			$this->getAbsoluteImagePath('caldav/attendees.png'),
470
			implode("\n", $attendeesText), '', IMipPlugin::IMIP_INDENT);
471
	}
472
473
	/**
474
	 * @param IEMailTemplate $template
475
	 * @param VEVENT $vevent
476
	 * @param $data
477
	 */
478
	public function addBulletList(IEMailTemplate $template, VEvent $vevent, $data) {
479
		$template->addBodyListItem(
480
			$data['meeting_title_html'] ?? $data['meeting_title'], $this->l10n->t('Title:'),
481
			$this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT);
482
		if ($data['meeting_when'] !== '') {
483
			$template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Time:'),
484
				$this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT);
485
		}
486
		if ($data['meeting_location'] !== '') {
487
			$template->addBodyListItem($data['meeting_location_html'] ?? $data['meeting_location'], $this->l10n->t('Location:'),
488
				$this->getAbsoluteImagePath('caldav/location.png'), $data['meeting_location'], '', IMipPlugin::IMIP_INDENT);
489
		}
490
		if ($data['meeting_url'] !== '') {
491
			$template->addBodyListItem($data['meeting_url_html'] ?? $data['meeting_url'], $this->l10n->t('Link:'),
492
				$this->getAbsoluteImagePath('caldav/link.png'), $data['meeting_url'], '', IMipPlugin::IMIP_INDENT);
493
		}
494
495
		$this->addAttendees($template, $vevent);
496
497
		/* Put description last, like an email body, since it can be arbitrarily long */
498
		if ($data['meeting_description']) {
499
			$template->addBodyListItem($data['meeting_description_html'] ?? $data['meeting_description'], $this->l10n->t('Description:'),
500
				$this->getAbsoluteImagePath('caldav/description.png'), $data['meeting_description'], '', IMipPlugin::IMIP_INDENT);
501
		}
502
	}
503
504
	/**
505
	 * @param Message $iTipMessage
506
	 * @return null|Property
507
	 */
508
	public function getCurrentAttendee(Message $iTipMessage): ?Property {
509
		/** @var VEvent $vevent */
510
		$vevent = $iTipMessage->message->VEVENT;
511
		$attendees = $vevent->select('ATTENDEE');
512
		foreach ($attendees as $attendee) {
513
			/** @var Property $attendee */
514
			if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
515
				return $attendee;
516
			}
517
		}
518
		return null;
519
	}
520
521
	/**
522
	 * @param Message $iTipMessage
523
	 * @param VEvent $vevent
524
	 * @param int $lastOccurrence
525
	 * @return string
526
	 */
527
	public function createInvitationToken(Message $iTipMessage, VEvent $vevent, int $lastOccurrence): string {
528
		$token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
529
530
		$attendee = $iTipMessage->recipient;
531
		$organizer = $iTipMessage->sender;
532
		$sequence = $iTipMessage->sequence;
533
		$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
534
			$vevent->{'RECURRENCE-ID'}->serialize() : null;
535
		$uid = $vevent->{'UID'};
536
537
		$query = $this->db->getQueryBuilder();
538
		$query->insert('calendar_invitations')
539
			->values([
540
				'token' => $query->createNamedParameter($token),
541
				'attendee' => $query->createNamedParameter($attendee),
542
				'organizer' => $query->createNamedParameter($organizer),
543
				'sequence' => $query->createNamedParameter($sequence),
544
				'recurrenceid' => $query->createNamedParameter($recurrenceId),
545
				'expiration' => $query->createNamedParameter($lastOccurrence),
546
				'uid' => $query->createNamedParameter($uid)
547
			])
548
			->execute();
549
550
		return $token;
551
	}
552
553
	/**
554
	 * Create a valid VCalendar object out of the details of
555
	 * a VEvent and its associated iTip Message
556
	 *
557
	 * We do this to filter out all unchanged VEvents
558
	 * This is especially important in iTip Messages with recurrences
559
	 * and recurrence exceptions
560
	 *
561
	 * @param Message $iTipMessage
562
	 * @param VEvent $vEvent
563
	 * @return VCalendar
564
	 */
565
	public function generateVCalendar(Message $iTipMessage, VEvent $vEvent): VCalendar {
566
		$vCalendar = new VCalendar();
567
		$vCalendar->add('METHOD', $iTipMessage->method);
568
		foreach ($iTipMessage->message->getComponents() as $component) {
569
			if ($component instanceof VEvent) {
570
				continue;
571
			}
572
			$vCalendar->add(clone $component);
573
		}
574
		$vCalendar->add($vEvent);
575
		return $vCalendar;
576
	}
577
578
	/**
579
	 * @param IEMailTemplate $template
580
	 * @param $token
581
	 */
582
	public function addResponseButtons(IEMailTemplate $template, $token) {
583
		$template->addBodyButtonGroup(
584
			$this->l10n->t('Accept'),
585
			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
586
				'token' => $token,
587
			]),
588
			$this->l10n->t('Decline'),
589
			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
590
				'token' => $token,
591
			])
592
		);
593
	}
594
595
	public function addMoreOptionsButton(IEMailTemplate $template, $token) {
596
		$moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
597
			'token' => $token,
598
		]);
599
		$html = vsprintf('<small><a href="%s">%s</a></small>', [
600
			$moreOptionsURL, $this->l10n->t('More options …')
601
		]);
602
		$text = $this->l10n->t('More options at %s', [$moreOptionsURL]);
603
604
		$template->addBodyText($html, $text);
605
	}
606
}
607