Passed
Push — master ( 5483d0...593d64 )
by John
35:02 queued 22:39
created

IMipPlugin::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 17
rs 9.8666
cc 1
nc 1
nop 11

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2017, Georg Ehrke
5
 *
6
 * @author Brad Rubenstein <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author Georg Ehrke <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Leon Klingele <[email protected]>
11
 * @author rakekniven <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 * @author Thomas Citharel <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 *
16
 * @license AGPL-3.0
17
 *
18
 * This code is free software: you can redistribute it and/or modify
19
 * it under the terms of the GNU Affero General Public License, version 3,
20
 * as published by the Free Software Foundation.
21
 *
22
 * This program is distributed in the hope that it will be useful,
23
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
 * GNU Affero General Public License for more details.
26
 *
27
 * You should have received a copy of the GNU Affero General Public License, version 3,
28
 * along with this program. If not, see <http://www.gnu.org/licenses/>
29
 *
30
 */
31
32
namespace OCA\DAV\CalDAV\Schedule;
33
34
use OCP\AppFramework\Utility\ITimeFactory;
35
use OCP\Defaults;
36
use OCP\IConfig;
37
use OCP\IDBConnection;
38
use OCP\IL10N;
39
use OCP\ILogger;
40
use OCP\IURLGenerator;
41
use OCP\IUserManager;
42
use OCP\L10N\IFactory as L10NFactory;
43
use OCP\Mail\IEMailTemplate;
44
use OCP\Mail\IMailer;
45
use OCP\Security\ISecureRandom;
46
use OCP\Util;
47
use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin;
48
use Sabre\VObject\Component\VCalendar;
49
use Sabre\VObject\Component\VEvent;
50
use Sabre\VObject\DateTimeParser;
51
use Sabre\VObject\ITip\Message;
52
use Sabre\VObject\Parameter;
53
use Sabre\VObject\Property;
54
use Sabre\VObject\Recur\EventIterator;
55
56
/**
57
 * iMIP handler.
58
 *
59
 * This class is responsible for sending out iMIP messages. iMIP is the
60
 * email-based transport for iTIP. iTIP deals with scheduling operations for
61
 * iCalendar objects.
62
 *
63
 * If you want to customize the email that gets sent out, you can do so by
64
 * extending this class and overriding the sendMessage method.
65
 *
66
 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
67
 * @author Evert Pot (http://evertpot.com/)
68
 * @license http://sabre.io/license/ Modified BSD License
69
 */
70
class IMipPlugin extends SabreIMipPlugin {
71
72
	/** @var string */
73
	private $userId;
74
75
	/** @var IConfig */
76
	private $config;
77
78
	/** @var IMailer */
79
	private $mailer;
80
81
	/** @var ILogger */
82
	private $logger;
83
84
	/** @var ITimeFactory */
85
	private $timeFactory;
86
87
	/** @var L10NFactory */
88
	private $l10nFactory;
89
90
	/** @var IURLGenerator */
91
	private $urlGenerator;
92
93
	/** @var ISecureRandom */
94
	private $random;
95
96
	/** @var IDBConnection */
97
	private $db;
98
99
	/** @var Defaults */
100
	private $defaults;
101
102
	/** @var IUserManager */
103
	private $userManager;
104
105
	public const MAX_DATE = '2038-01-01';
106
107
	public const METHOD_REQUEST = 'request';
108
	public const METHOD_REPLY = 'reply';
109
	public const METHOD_CANCEL = 'cancel';
110
	public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages
111
112
	/**
113
	 * @param IConfig $config
114
	 * @param IMailer $mailer
115
	 * @param ILogger $logger
116
	 * @param ITimeFactory $timeFactory
117
	 * @param L10NFactory $l10nFactory
118
	 * @param IUrlGenerator $urlGenerator
119
	 * @param Defaults $defaults
120
	 * @param ISecureRandom $random
121
	 * @param IDBConnection $db
122
	 * @param string $userId
123
	 */
124
	public function __construct(IConfig $config, IMailer $mailer, ILogger $logger,
125
								ITimeFactory $timeFactory, L10NFactory $l10nFactory,
126
								IURLGenerator $urlGenerator, Defaults $defaults,
127
								ISecureRandom $random, IDBConnection $db, IUserManager $userManager,
128
								$userId) {
129
		parent::__construct('');
130
		$this->userId = $userId;
131
		$this->config = $config;
132
		$this->mailer = $mailer;
133
		$this->logger = $logger;
134
		$this->timeFactory = $timeFactory;
135
		$this->l10nFactory = $l10nFactory;
136
		$this->urlGenerator = $urlGenerator;
137
		$this->random = $random;
138
		$this->db = $db;
139
		$this->defaults = $defaults;
140
		$this->userManager = $userManager;
141
	}
142
143
	/**
144
	 * Event handler for the 'schedule' event.
145
	 *
146
	 * @param Message $iTipMessage
147
	 * @return void
148
	 */
149
	public function schedule(Message $iTipMessage) {
150
151
		// Not sending any emails if the system considers the update
152
		// insignificant.
153
		if (!$iTipMessage->significantChange) {
154
			if (!$iTipMessage->scheduleStatus) {
155
				$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
156
			}
157
			return;
158
		}
159
160
		$summary = $iTipMessage->message->VEVENT->SUMMARY;
0 ignored issues
show
Bug introduced by
The property SUMMARY does not seem to exist on Sabre\VObject\Property.
Loading history...
161
162
		if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') {
163
			return;
164
		}
165
166
		if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
167
			return;
168
		}
169
170
		// don't send out mails for events that already took place
171
		$lastOccurrence = $this->getLastOccurrence($iTipMessage->message);
172
		$currentTime = $this->timeFactory->getTime();
173
		if ($lastOccurrence < $currentTime) {
174
			return;
175
		}
176
177
		// Strip off mailto:
178
		$sender = substr($iTipMessage->sender, 7);
179
		$recipient = substr($iTipMessage->recipient, 7);
180
181
		$senderName = $iTipMessage->senderName ?: null;
182
		$recipientName = $iTipMessage->recipientName ?: null;
183
184
		if ($senderName === null || empty(trim($senderName))) {
185
			$user = $this->userManager->get($this->userId);
186
			if ($user) {
187
				// getDisplayName automatically uses the uid
188
				// if no display-name is set
189
				$senderName = $user->getDisplayName();
190
			}
191
		}
192
193
		/** @var VEvent $vevent */
194
		$vevent = $iTipMessage->message->VEVENT;
195
196
		$attendee = $this->getCurrentAttendee($iTipMessage);
197
		$defaultLang = $this->l10nFactory->findLanguage();
198
		$lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee);
199
		$l10n = $this->l10nFactory->get('dav', $lang);
200
201
		$meetingAttendeeName = $recipientName ?: $recipient;
202
		$meetingInviteeName = $senderName ?: $sender;
203
204
		$meetingTitle = $vevent->SUMMARY;
205
		$meetingDescription = $vevent->DESCRIPTION;
206
207
208
		$meetingUrl = $vevent->URL;
209
		$meetingLocation = $vevent->LOCATION;
0 ignored issues
show
Unused Code introduced by
The assignment to $meetingLocation is dead and can be removed.
Loading history...
210
211
		$defaultVal = '--';
212
213
		$method = self::METHOD_REQUEST;
214
		switch (strtolower($iTipMessage->method)) {
215
			case self::METHOD_REPLY:
216
				$method = self::METHOD_REPLY;
217
				break;
218
			case self::METHOD_CANCEL:
219
				$method = self::METHOD_CANCEL;
220
				break;
221
		}
222
223
		$data = [
224
			'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
225
			'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
226
			'meeting_title' => (string)$meetingTitle ?: $defaultVal,
227
			'meeting_description' => (string)$meetingDescription ?: $defaultVal,
228
			'meeting_url' => (string)$meetingUrl ?: $defaultVal,
229
		];
230
231
		$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
232
		$fromName = $l10n->t('%1$s via %2$s', [$senderName, $this->defaults->getName()]);
233
234
		$message = $this->mailer->createMessage()
235
			->setFrom([$fromEMail => $fromName])
236
			->setReplyTo([$sender => $senderName])
237
			->setTo([$recipient => $recipientName]);
238
239
		$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
240
		$template->addHeader();
241
242
		$summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
243
244
		$this->addSubjectAndHeading($template, $l10n, $method, $summary);
245
		$this->addBulletList($template, $l10n, $vevent);
246
247
248
		// Only add response buttons to invitation requests: Fix Issue #11230
249
		if (($method == self::METHOD_REQUEST) && $this->getAttendeeRSVP($attendee)) {
250
251
			/*
252
			** Only offer invitation accept/reject buttons, which link back to the
253
			** nextcloud server, to recipients who can access the nextcloud server via
254
			** their internet/intranet.  Issue #12156
255
			**
256
			** The app setting is stored in the appconfig database table.
257
			**
258
			** For nextcloud servers accessible to the public internet, the default
259
			** "invitation_link_recipients" value "yes" (all recipients) is appropriate.
260
			**
261
			** When the nextcloud server is restricted behind a firewall, accessible
262
			** only via an internal network or via vpn, you can set "dav.invitation_link_recipients"
263
			** to the email address or email domain, or comma separated list of addresses or domains,
264
			** of recipients who can access the server.
265
			**
266
			** To always deliver URLs, set invitation_link_recipients to "yes".
267
			** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
268
			*/
269
270
			$recipientDomain = substr(strrchr($recipient, "@"), 1);
271
			$invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
272
273
			if (strcmp('yes', $invitationLinkRecipients[0]) === 0
274
				 || in_array(strtolower($recipient), $invitationLinkRecipients)
275
				 || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
276
				$this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence);
277
			}
278
		}
279
280
		$template->addFooter();
281
282
		$message->useTemplate($template);
283
284
		$attachment = $this->mailer->createAttachment(
285
			$iTipMessage->message->serialize(),
286
			'event.ics',// TODO(leon): Make file name unique, e.g. add event id
287
			'text/calendar; method=' . $iTipMessage->method
288
		);
289
		$message->attach($attachment);
290
291
		try {
292
			$failed = $this->mailer->send($message);
293
			$iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
294
			if ($failed) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $failed of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
295
				$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' =>  implode(', ', $failed)]);
296
				$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
297
			}
298
		} catch (\Exception $ex) {
299
			$this->logger->logException($ex, ['app' => 'dav']);
300
			$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
301
		}
302
	}
303
304
	/**
305
	 * check if event took place in the past already
306
	 * @param VCalendar $vObject
307
	 * @return int
308
	 */
309
	private function getLastOccurrence(VCalendar $vObject) {
310
		/** @var VEvent $component */
311
		$component = $vObject->VEVENT;
312
313
		$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
0 ignored issues
show
Bug introduced by
The method getDateTime() 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\DateTime or Sabre\VObject\Property\VCard\DateAndOrTime. ( Ignorable by Annotation )

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

313
		$firstOccurrence = $component->DTSTART->/** @scrutinizer ignore-call */ getDateTime()->getTimeStamp();
Loading history...
314
		// Finding the last occurrence is a bit harder
315
		if (!isset($component->RRULE)) {
316
			if (isset($component->DTEND)) {
317
				$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
318
			} elseif (isset($component->DURATION)) {
319
				/** @var \DateTime $endDate */
320
				$endDate = clone $component->DTSTART->getDateTime();
321
				// $component->DTEND->getDateTime() returns DateTimeImmutable
322
				$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
323
				$lastOccurrence = $endDate->getTimestamp();
324
			} elseif (!$component->DTSTART->hasTime()) {
0 ignored issues
show
Bug introduced by
The method hasTime() 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\DateTime. ( Ignorable by Annotation )

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

324
			} elseif (!$component->DTSTART->/** @scrutinizer ignore-call */ hasTime()) {
Loading history...
325
				/** @var \DateTime $endDate */
326
				$endDate = clone $component->DTSTART->getDateTime();
327
				// $component->DTSTART->getDateTime() returns DateTimeImmutable
328
				$endDate = $endDate->modify('+1 day');
329
				$lastOccurrence = $endDate->getTimestamp();
330
			} else {
331
				$lastOccurrence = $firstOccurrence;
332
			}
333
		} else {
334
			$it = new EventIterator($vObject, (string)$component->UID);
335
			$maxDate = new \DateTime(self::MAX_DATE);
336
			if ($it->isInfinite()) {
337
				$lastOccurrence = $maxDate->getTimestamp();
338
			} else {
339
				$end = $it->getDtEnd();
340
				while ($it->valid() && $end < $maxDate) {
341
					$end = $it->getDtEnd();
342
					$it->next();
343
				}
344
				$lastOccurrence = $end->getTimestamp();
345
			}
346
		}
347
348
		return $lastOccurrence;
349
	}
350
351
	/**
352
	 * @param Message $iTipMessage
353
	 * @return null|Property
354
	 */
355
	private function getCurrentAttendee(Message $iTipMessage) {
356
		/** @var VEvent $vevent */
357
		$vevent = $iTipMessage->message->VEVENT;
358
		$attendees = $vevent->select('ATTENDEE');
359
		foreach ($attendees as $attendee) {
360
			/** @var Property $attendee */
361
			if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
362
				return $attendee;
363
			}
364
		}
365
		return null;
366
	}
367
368
	/**
369
	 * @param string $default
370
	 * @param Property|null $attendee
371
	 * @return string
372
	 */
373
	private function getAttendeeLangOrDefault($default, Property $attendee = null) {
374
		if ($attendee !== null) {
375
			$lang = $attendee->offsetGet('LANGUAGE');
376
			if ($lang instanceof Parameter) {
377
				return $lang->getValue();
378
			}
379
		}
380
		return $default;
381
	}
382
383
	/**
384
	 * @param Property|null $attendee
385
	 * @return bool
386
	 */
387
	private function getAttendeeRSVP(Property $attendee = null) {
388
		if ($attendee !== null) {
389
			$rsvp = $attendee->offsetGet('RSVP');
390
			if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
391
				return true;
392
			}
393
		}
394
		// RFC 5545 3.2.17: default RSVP is false
395
		return false;
396
	}
397
398
	/**
399
	 * @param IL10N $l10n
400
	 * @param VEvent $vevent
401
	 */
402
	private function generateWhenString(IL10N $l10n, VEvent $vevent) {
403
		$dtstart = $vevent->DTSTART;
404
		if (isset($vevent->DTEND)) {
405
			$dtend = $vevent->DTEND;
406
		} elseif (isset($vevent->DURATION)) {
407
			$isFloating = $vevent->DTSTART->isFloating();
0 ignored issues
show
Bug introduced by
The method isFloating() 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\DateTime. ( Ignorable by Annotation )

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

407
			/** @scrutinizer ignore-call */ 
408
   $isFloating = $vevent->DTSTART->isFloating();
Loading history...
408
			$dtend = clone $vevent->DTSTART;
409
			$endDateTime = $dtend->getDateTime();
410
			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
411
			$dtend->setDateTime($endDateTime, $isFloating);
0 ignored issues
show
Bug introduced by
The method setDateTime() 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\DateTime or Sabre\VObject\Property\VCard\DateAndOrTime. ( Ignorable by Annotation )

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

411
			$dtend->/** @scrutinizer ignore-call */ 
412
           setDateTime($endDateTime, $isFloating);
Loading history...
412
		} elseif (!$vevent->DTSTART->hasTime()) {
413
			$isFloating = $vevent->DTSTART->isFloating();
414
			$dtend = clone $vevent->DTSTART;
415
			$endDateTime = $dtend->getDateTime();
416
			$endDateTime = $endDateTime->modify('+1 day');
417
			$dtend->setDateTime($endDateTime, $isFloating);
418
		} else {
419
			$dtend = clone $vevent->DTSTART;
420
		}
421
422
		$isAllDay = $dtstart instanceof Property\ICalendar\Date;
423
424
		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
425
		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
426
		/** @var \DateTimeImmutable $dtstartDt */
427
		$dtstartDt = $dtstart->getDateTime();
428
		/** @var \DateTimeImmutable $dtendDt */
429
		$dtendDt = $dtend->getDateTime();
430
431
		$diff = $dtstartDt->diff($dtendDt);
432
433
		$dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
434
		$dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
435
436
		if ($isAllDay) {
437
			// One day event
438
			if ($diff->days === 1) {
439
				return $l10n->l('date', $dtstartDt, ['width' => 'medium']);
440
			}
441
442
			// DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
443
			// the email should show 2020-01-01 to 2020-01-04.
444
			$dtendDt->modify('-1 day');
445
446
			//event that spans over multiple days
447
			$localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
448
			$localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
449
450
			return $localeStart . ' - ' . $localeEnd;
0 ignored issues
show
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

450
			return $localeStart . ' - ' . /** @scrutinizer ignore-type */ $localeEnd;
Loading history...
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

450
			return /** @scrutinizer ignore-type */ $localeStart . ' - ' . $localeEnd;
Loading history...
451
		}
452
453
		/** @var Property\ICalendar\DateTime $dtstart */
454
		/** @var Property\ICalendar\DateTime $dtend */
455
		$isFloating = $dtstart->isFloating();
456
		$startTimezone = $endTimezone = null;
457
		if (!$isFloating) {
458
			$prop = $dtstart->offsetGet('TZID');
459
			if ($prop instanceof Parameter) {
460
				$startTimezone = $prop->getValue();
461
			}
462
463
			$prop = $dtend->offsetGet('TZID');
464
			if ($prop instanceof Parameter) {
465
				$endTimezone = $prop->getValue();
466
			}
467
		}
468
469
		$localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
0 ignored issues
show
Bug introduced by
Are you sure $l10n->l('weekdayName', ...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

469
		$localeStart = /** @scrutinizer ignore-type */ $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
Loading history...
470
			$l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
0 ignored issues
show
Bug introduced by
Are you sure $l10n->l('datetime', $dt...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

470
			/** @scrutinizer ignore-type */ $l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
Loading history...
471
472
		// always show full date with timezone if timezones are different
473
		if ($startTimezone !== $endTimezone) {
474
			$localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
475
476
			return $localeStart . ' (' . $startTimezone . ') - ' .
477
				$localeEnd . ' (' . $endTimezone . ')';
478
		}
479
480
		// show only end time if date is the same
481
		if ($this->isDayEqual($dtstartDt, $dtendDt)) {
482
			$localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
483
		} else {
484
			$localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
0 ignored issues
show
Bug introduced by
Are you sure $l10n->l('weekdayName', ...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

484
			$localeEnd = /** @scrutinizer ignore-type */ $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
Loading history...
485
				$l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
0 ignored issues
show
Bug introduced by
Are you sure $l10n->l('datetime', $dt...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

485
				/** @scrutinizer ignore-type */ $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
Loading history...
486
		}
487
488
		return  $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
489
	}
490
491
	/**
492
	 * @param \DateTime $dtStart
493
	 * @param \DateTime $dtEnd
494
	 * @return bool
495
	 */
496
	private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) {
497
		return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
498
	}
499
500
	/**
501
	 * @param IEMailTemplate $template
502
	 * @param IL10N $l10n
503
	 * @param string $method
504
	 * @param string $summary
505
	 */
506
	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
507
										  $method, $summary) {
508
		if ($method === self::METHOD_CANCEL) {
509
			$template->setSubject('Canceled: ' . $summary);
510
			$template->addHeading($l10n->t('Invitation canceled'));
511
		} elseif ($method === self::METHOD_REPLY) {
512
			$template->setSubject('Re: ' . $summary);
513
			$template->addHeading($l10n->t('Invitation updated'));
514
		} else {
515
			$template->setSubject('Invitation: ' . $summary);
516
			$template->addHeading($l10n->t('Invitation'));
517
		}
518
	}
519
520
	/**
521
	 * @param IEMailTemplate $template
522
	 * @param IL10N $l10n
523
	 * @param VEVENT $vevent
524
	 */
525
	private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) {
526
		if ($vevent->SUMMARY) {
527
			$template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'),
528
				$this->getAbsoluteImagePath('caldav/title.svg'),'','',self::IMIP_INDENT);
529
		}
530
		$meetingWhen = $this->generateWhenString($l10n, $vevent);
531
		if ($meetingWhen) {
532
			$template->addBodyListItem($meetingWhen, $l10n->t('Time:'),
533
				$this->getAbsoluteImagePath('caldav/time.svg'),'','',self::IMIP_INDENT);
534
		}
535
		if ($vevent->LOCATION) {
536
			$template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'),
537
				$this->getAbsoluteImagePath('caldav/location.svg'),'','',self::IMIP_INDENT);
538
		}
539
		if ($vevent->URL) {
540
			$url = $vevent->URL->getValue();
541
			$template->addBodyListItem(sprintf('<a href="%s">%s</a>',
542
					htmlspecialchars($url),
543
					htmlspecialchars($url)),
544
				$l10n->t('Link:'),
545
				$this->getAbsoluteImagePath('caldav/link.svg'),
546
				$url,'',self::IMIP_INDENT);
547
		}
548
549
		$this->addAttendees($template, $l10n, $vevent);
550
551
		/* Put description last, like an email body, since it can be arbitrarily long */
552
		if ($vevent->DESCRIPTION) {
553
			$template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'),
554
				$this->getAbsoluteImagePath('caldav/description.svg'),'','',self::IMIP_INDENT);
555
		}
556
	}
557
558
	/**
559
	 * addAttendees: add organizer and attendee names/emails to iMip mail.
560
	 *
561
	 * Enable with DAV setting: invitation_list_attendees (default: no)
562
	 *
563
	 * The default is 'no', which matches old behavior, and is privacy preserving.
564
	 *
565
	 * To enable including attendees in invitation emails:
566
	 *   % php occ config:app:set dav invitation_list_attendees --value yes
567
	 *
568
	 * @param IEMailTemplate $template
569
	 * @param IL10N $l10n
570
	 * @param Message $iTipMessage
571
	 * @param int $lastOccurrence
572
	 * @author brad2014 on github.com
573
	 */
574
575
	private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) {
576
		if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
577
			return;
578
		}
579
580
		if (isset($vevent->ORGANIZER)) {
581
			/** @var Property\ICalendar\CalAddress $organizer */
582
			$organizer = $vevent->ORGANIZER;
583
			$organizerURI = $organizer->getNormalizedValue();
584
			list($scheme,$organizerEmail) = explode(':',$organizerURI,2); # strip off scheme mailto:
585
			/** @var string|null $organizerName */
586
			$organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
587
			$organizerHTML = sprintf('<a href="%s">%s</a>',
588
				htmlspecialchars($organizerURI),
589
				htmlspecialchars($organizerName ?: $organizerEmail));
590
			$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
591
			if (isset($organizer['PARTSTAT'])) {
592
				/** @var Parameter $partstat */
593
				$partstat = $organizer['PARTSTAT'];
594
				if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
595
					$organizerHTML .= ' ✔︎';
596
					$organizerText .= ' ✔︎';
597
				}
598
			}
599
			$template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'),
600
				$this->getAbsoluteImagePath('caldav/organizer.svg'),
601
				$organizerText,'',self::IMIP_INDENT);
602
		}
603
604
		$attendees = $vevent->select('ATTENDEE');
605
		if (count($attendees) === 0) {
606
			return;
607
		}
608
609
		$attendeesHTML = [];
610
		$attendeesText = [];
611
		foreach ($attendees as $attendee) {
612
			$attendeeURI = $attendee->getNormalizedValue();
613
			list($scheme,$attendeeEmail) = explode(':',$attendeeURI,2); # strip off scheme mailto:
614
			$attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
615
			$attendeeHTML = sprintf('<a href="%s">%s</a>',
616
				htmlspecialchars($attendeeURI),
617
				htmlspecialchars($attendeeName ?: $attendeeEmail));
618
			$attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
619
			if (isset($attendee['PARTSTAT'])
620
				&& strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) {
621
				$attendeeHTML .= ' ✔︎';
622
				$attendeeText .= ' ✔︎';
623
			}
624
			array_push($attendeesHTML, $attendeeHTML);
625
			array_push($attendeesText, $attendeeText);
626
		}
627
628
		$template->addBodyListItem(implode('<br/>',$attendeesHTML), $l10n->t('Attendees:'),
629
			$this->getAbsoluteImagePath('caldav/attendees.svg'),
630
			implode("\n",$attendeesText),'',self::IMIP_INDENT);
631
	}
632
633
	/**
634
	 * @param IEMailTemplate $template
635
	 * @param IL10N $l10n
636
	 * @param Message $iTipMessage
637
	 * @param int $lastOccurrence
638
	 */
639
	private function addResponseButtons(IEMailTemplate $template, IL10N $l10n,
640
										Message $iTipMessage, $lastOccurrence) {
641
		$token = $this->createInvitationToken($iTipMessage, $lastOccurrence);
642
643
		$template->addBodyButtonGroup(
644
			$l10n->t('Accept'),
645
			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
646
				'token' => $token,
647
			]),
648
			$l10n->t('Decline'),
649
			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
650
				'token' => $token,
651
			])
652
		);
653
654
		$moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
655
			'token' => $token,
656
		]);
657
		$html = vsprintf('<small><a href="%s">%s</a></small>', [
658
			$moreOptionsURL, $l10n->t('More options …')
659
		]);
660
		$text = $l10n->t('More options at %s', [$moreOptionsURL]);
661
662
		$template->addBodyText($html, $text);
663
	}
664
665
	/**
666
	 * @param string $path
667
	 * @return string
668
	 */
669
	private function getAbsoluteImagePath($path) {
670
		return $this->urlGenerator->getAbsoluteURL(
671
			$this->urlGenerator->imagePath('core', $path)
672
		);
673
	}
674
675
	/**
676
	 * @param Message $iTipMessage
677
	 * @param int $lastOccurrence
678
	 * @return string
679
	 */
680
	private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
681
		$token = $this->random->generate(60, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
682
683
		/** @var VEvent $vevent */
684
		$vevent = $iTipMessage->message->VEVENT;
685
		$attendee = $iTipMessage->recipient;
686
		$organizer = $iTipMessage->sender;
687
		$sequence = $iTipMessage->sequence;
688
		$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
689
			$vevent->{'RECURRENCE-ID'}->serialize() : null;
690
		$uid = $vevent->{'UID'};
691
692
		$query = $this->db->getQueryBuilder();
693
		$query->insert('calendar_invitations')
694
			->values([
695
				'token' => $query->createNamedParameter($token),
696
				'attendee' => $query->createNamedParameter($attendee),
697
				'organizer' => $query->createNamedParameter($organizer),
698
				'sequence' => $query->createNamedParameter($sequence),
699
				'recurrenceid' => $query->createNamedParameter($recurrenceId),
700
				'expiration' => $query->createNamedParameter($lastOccurrence),
701
				'uid' => $query->createNamedParameter($uid)
702
			])
703
			->execute();
704
705
		return $token;
706
	}
707
}
708