Completed
Push — master ( 0abb97...6c29ce )
by Morris
16:38
created

IMipPlugin::isDayEqual()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2017, Georg Ehrke
5
 *
6
 * @author Thomas Müller <[email protected]>
7
 * @author Georg Ehrke <[email protected]>
8
 *
9
 * @license AGPL-3.0
10
 *
11
 * This code is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License, version 3,
13
 * as published by the Free Software Foundation.
14
 *
15
 * This program 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 License, version 3,
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
22
 *
23
 */
24
namespace OCA\DAV\CalDAV\Schedule;
25
26
use OCP\AppFramework\Utility\ITimeFactory;
27
use OCP\IConfig;
28
use OCP\IL10N;
29
use OCP\ILogger;
30
use OCP\IURLGenerator;
31
use OCP\L10N\IFactory as L10NFactory;
32
use OCP\Mail\IEMailTemplate;
33
use OCP\Mail\IMailer;
34
use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin;
35
use Sabre\DAV\Xml\Element\Prop;
36
use Sabre\VObject\Component\VCalendar;
37
use Sabre\VObject\Component\VEvent;
38
use Sabre\VObject\DateTimeParser;
39
use Sabre\VObject\ITip\Message;
40
use Sabre\VObject\Parameter;
41
use Sabre\VObject\Property;
42
use Sabre\VObject\Recur\EventIterator;
43
/**
44
 * iMIP handler.
45
 *
46
 * This class is responsible for sending out iMIP messages. iMIP is the
47
 * email-based transport for iTIP. iTIP deals with scheduling operations for
48
 * iCalendar objects.
49
 *
50
 * If you want to customize the email that gets sent out, you can do so by
51
 * extending this class and overriding the sendMessage method.
52
 *
53
 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
54
 * @author Evert Pot (http://evertpot.com/)
55
 * @license http://sabre.io/license/ Modified BSD License
56
 */
57
class IMipPlugin extends SabreIMipPlugin {
58
59
	/** @var string */
60
	private $userId;
61
62
	/** @var IConfig */
63
	private $config;
64
65
	/** @var IMailer */
66
	private $mailer;
67
68
	/** @var ILogger */
69
	private $logger;
70
71
	/** @var ITimeFactory */
72
	private $timeFactory;
73
74
	/** @var L10NFactory */
75
	private $l10nFactory;
76
77
	/** @var IURLGenerator */
78
	private $urlGenerator;
79
80
	const MAX_DATE = '2038-01-01';
81
82
	const METHOD_REQUEST = 'request';
83
	const METHOD_REPLY = 'reply';
84
	const METHOD_CANCEL = 'cancel';
85
86
	/**
87
	 * @param IConfig $config
88
	 * @param IMailer $mailer
89
	 * @param ILogger $logger
90
	 * @param ITimeFactory $timeFactory
91
	 * @param L10NFactory $l10nFactory
92
	 * @param IUrlGenerator $urlGenerator
93
	 * @param string $userId
94
	 */
95 View Code Duplication
	public function __construct(IConfig $config, IMailer $mailer, ILogger $logger, ITimeFactory $timeFactory, L10NFactory $l10nFactory, IURLGenerator $urlGenerator, $userId) {
96
		parent::__construct('');
97
		$this->userId = $userId;
98
		$this->config = $config;
99
		$this->mailer = $mailer;
100
		$this->logger = $logger;
101
		$this->timeFactory = $timeFactory;
102
		$this->l10nFactory = $l10nFactory;
103
		$this->urlGenerator = $urlGenerator;
104
	}
105
106
	/**
107
	 * Event handler for the 'schedule' event.
108
	 *
109
	 * @param Message $iTipMessage
110
	 * @return void
111
	 */
112
	public function schedule(Message $iTipMessage) {
113
114
		// Not sending any emails if the system considers the update
115
		// insignificant.
116
		if (!$iTipMessage->significantChange) {
117
			if (!$iTipMessage->scheduleStatus) {
118
				$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
119
			}
120
			return;
121
		}
122
123
		$summary = $iTipMessage->message->VEVENT->SUMMARY;
124
125
		if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') {
126
			return;
127
		}
128
129
		if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
130
			return;
131
		}
132
133
		// don't send out mails for events that already took place
134
		if ($this->isEventInThePast($iTipMessage->message)) {
135
			return;
136
		}
137
138
		// Strip off mailto:
139
		$sender = substr($iTipMessage->sender, 7);
140
		$recipient = substr($iTipMessage->recipient, 7);
141
142
		$senderName = $iTipMessage->senderName ?: null;
143
		$recipientName = $iTipMessage->recipientName ?: null;
144
145
		/** @var VEvent $vevent */
146
		$vevent = $iTipMessage->message->VEVENT;
147
148
		$attendee = $this->getCurrentAttendee($iTipMessage);
149
		$defaultLang = $this->config->getUserValue($this->userId, 'core', 'lang', $this->l10nFactory->findLanguage());
150
		$lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee);
151
		$l10n = $this->l10nFactory->get('dav', $lang);
152
153
		$meetingAttendeeName = $recipientName ?: $recipient;
154
		$meetingInviteeName = $senderName ?: $sender;
155
156
		$meetingTitle = $vevent->SUMMARY;
157
		$meetingDescription = $vevent->DESCRIPTION;
158
159
		$start = $vevent->DTSTART;
160
		if (isset($vevent->DTEND)) {
161
			$end = $vevent->DTEND;
162
		} elseif (isset($vevent->DURATION)) {
163
			$isFloating = $vevent->DTSTART->isFloating();
164
			$end = clone $vevent->DTSTART;
165
			$endDateTime = $end->getDateTime();
166
			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
167
			$end->setDateTime($endDateTime, $isFloating);
168
		} elseif (!$vevent->DTSTART->hasTime()) {
169
			$isFloating = $vevent->DTSTART->isFloating();
170
			$end = clone $vevent->DTSTART;
171
			$endDateTime = $end->getDateTime();
172
			$endDateTime = $endDateTime->modify('+1 day');
173
			$end->setDateTime($endDateTime, $isFloating);
174
		} else {
175
			$end = clone $vevent->DTSTART;
176
		}
177
178
		$meetingWhen = $this->generateWhenString($l10n, $start, $end);
179
180
		$meetingUrl = $vevent->URL;
181
		$meetingLocation = $vevent->LOCATION;
182
183
		$defaultVal = '--';
184
185
		$method = self::METHOD_REQUEST;
186
		switch (strtolower($iTipMessage->method)) {
187
			case self::METHOD_REPLY:
188
				$method = self::METHOD_REPLY;
189
				break;
190
			case self::METHOD_CANCEL:
191
				$method = self::METHOD_CANCEL;
192
				break;
193
		}
194
195
		$data = array(
196
			'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
197
			'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
198
			'meeting_title' => (string)$meetingTitle ?: $defaultVal,
199
			'meeting_description' => (string)$meetingDescription ?: $defaultVal,
200
			'meeting_url' => (string)$meetingUrl ?: $defaultVal,
201
		);
202
203
		$message = $this->mailer->createMessage()
204
			->setReplyTo([$sender => $senderName])
205
			->setTo([$recipient => $recipientName]);
206
207
		$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
208
		$template->addHeader();
209
210
		$this->addSubjectAndHeading($template, $l10n, $method, $summary,
211
			$meetingAttendeeName, $meetingInviteeName);
212
		$this->addBulletList($template, $l10n, $meetingWhen, $meetingLocation,
213
			$meetingDescription, $meetingUrl);
214
215
		$template->addFooter();
216
		$message->useTemplate($template);
217
218
		$attachment = $this->mailer->createAttachment(
219
			$iTipMessage->message->serialize(),
220
			'event.ics',// TODO(leon): Make file name unique, e.g. add event id
221
			'text/calendar; method=' . $iTipMessage->method
222
		);
223
		$message->attach($attachment);
224
225
		try {
226
			$failed = $this->mailer->send($message);
227
			$iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
228
			if ($failed) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $failed of type string[] 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...
229
				$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' =>  implode(', ', $failed)]);
230
				$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
231
			}
232
		} catch(\Exception $ex) {
233
			$this->logger->logException($ex, ['app' => 'dav']);
234
			$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
235
		}
236
	}
237
238
	/**
239
	 * check if event took place in the past already
240
	 * @param VCalendar $vObject
241
	 * @return bool
242
	 */
243
	private function isEventInThePast(VCalendar $vObject) {
244
		/** @var VEvent $component */
245
		$component = $vObject->VEVENT;
246
247
		$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
248
		// Finding the last occurrence is a bit harder
249 View Code Duplication
		if (!isset($component->RRULE)) {
250
			if (isset($component->DTEND)) {
251
				$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
252
			} elseif (isset($component->DURATION)) {
253
				/** @var \DateTime $endDate */
254
				$endDate = clone $component->DTSTART->getDateTime();
255
				// $component->DTEND->getDateTime() returns DateTimeImmutable
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
256
				$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
257
				$lastOccurrence = $endDate->getTimestamp();
258
			} elseif (!$component->DTSTART->hasTime()) {
259
				/** @var \DateTime $endDate */
260
				$endDate = clone $component->DTSTART->getDateTime();
261
				// $component->DTSTART->getDateTime() returns DateTimeImmutable
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
262
				$endDate = $endDate->modify('+1 day');
263
				$lastOccurrence = $endDate->getTimestamp();
264
			} else {
265
				$lastOccurrence = $firstOccurrence;
266
			}
267
		} else {
268
			$it = new EventIterator($vObject, (string)$component->UID);
269
			$maxDate = new \DateTime(self::MAX_DATE);
270
			if ($it->isInfinite()) {
271
				$lastOccurrence = $maxDate->getTimestamp();
272
			} else {
273
				$end = $it->getDtEnd();
274
				while($it->valid() && $end < $maxDate) {
275
					$end = $it->getDtEnd();
276
					$it->next();
277
278
				}
279
				$lastOccurrence = $end->getTimestamp();
280
			}
281
		}
282
283
		$currentTime = $this->timeFactory->getTime();
284
		return $lastOccurrence < $currentTime;
285
	}
286
287
288
	/**
289
	 * @param Message $iTipMessage
290
	 * @return null|Property
291
	 */
292
	private function getCurrentAttendee(Message $iTipMessage) {
293
		/** @var VEvent $vevent */
294
		$vevent = $iTipMessage->message->VEVENT;
295
		$attendees = $vevent->select('ATTENDEE');
296
		foreach ($attendees as $attendee) {
297
			/** @var Property $attendee */
298
			if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
299
				return $attendee;
300
			}
301
		}
302
		return null;
303
	}
304
305
	/**
306
	 * @param string $default
307
	 * @param Property|null $attendee
308
	 * @return string
309
	 */
310
	private function getAttendeeLangOrDefault($default, Property $attendee = null) {
311
		if ($attendee !== null) {
312
			$lang = $attendee->offsetGet('LANGUAGE');
313
			if ($lang instanceof Parameter) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\Parameter does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
314
				return $lang->getValue();
315
			}
316
		}
317
		return $default;
318
	}
319
320
	/**
321
	 * @param IL10N $l10n
322
	 * @param Property $dtstart
323
	 * @param Property $dtend
324
	 */
325
	private function generateWhenString(IL10N $l10n, Property $dtstart, Property $dtend) {
326
		$isAllDay = $dtstart instanceof Property\ICalendar\Date;
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\Property\ICalendar\Date does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
327
328
		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
329
		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
330
		/** @var \DateTimeImmutable $dtstartDt */
331
		$dtstartDt = $dtstart->getDateTime();
332
		/** @var \DateTimeImmutable $dtendDt */
333
		$dtendDt = $dtend->getDateTime();
334
335
		$diff = $dtstartDt->diff($dtendDt);
336
337
		$dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
338
		$dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
339
340
		if ($isAllDay) {
341
			// One day event
342
			if ($diff->days === 1) {
343
				return $l10n->l('date', $dtstartDt, ['width' => 'medium']);
344
			}
345
346
			//event that spans over multiple days
347
			$localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
348
			$localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
349
350
			return $localeStart . ' - ' . $localeEnd;
351
		}
352
353
		/** @var Property\ICalendar\DateTime $dtstart */
354
		/** @var Property\ICalendar\DateTime $dtend */
355
		$isFloating = $dtstart->isFloating();
356
		$startTimezone = $endTimezone = null;
357
		if (!$isFloating) {
358
			$prop = $dtstart->offsetGet('TZID');
359
			if ($prop instanceof Parameter) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\Parameter does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
360
				$startTimezone = $prop->getValue();
361
			}
362
363
			$prop = $dtend->offsetGet('TZID');
364
			if ($prop instanceof Parameter) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\Parameter does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
365
				$endTimezone = $prop->getValue();
366
			}
367
		}
368
369
		$localeStart = $l10n->l('datetime', $dtstartDt, ['width' => 'medium']);
370
371
		// always show full date with timezone if timezones are different
372
		if ($startTimezone !== $endTimezone) {
373
			$localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium']);
374
375
			return $localeStart . ' (' . $startTimezone . ') - ' .
376
				$localeEnd . ' (' . $endTimezone . ')';
377
		}
378
379
		// show only end time if date is the same
380
		if ($this->isDayEqual($dtstartDt, $dtendDt)) {
381
			$localeEnd = $l10n->l('time', $dtendDt, ['width' => 'medium']);
382
		} else {
383
			$localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium']);
384
		}
385
386
		return  $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
387
	}
388
389
	/**
390
	 * @param \DateTime $dtStart
391
	 * @param \DateTime $dtEnd
392
	 * @return bool
393
	 */
394
	private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) {
395
		return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
396
	}
397
398
	/**
399
	 * @param IEMailTemplate $template
400
	 * @param IL10N $l10n
401
	 * @param string $method
402
	 * @param string $summary
403
	 * @param string $attendeeName
404
	 * @param string $inviteeName
405
	 */
406
	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
407
										  $method, $summary, $attendeeName, $inviteeName) {
408
		if ($method === self::METHOD_CANCEL) {
409
			$template->setSubject('Cancelled: ' . $summary);
410
			$template->addHeading($l10n->t('Invitation canceled'), $l10n->t('Hello %s,', [$attendeeName]));
411
			$template->addBodyText($l10n->t('The meeting »%s« with %s was canceled.', [$summary, $inviteeName]));
412
		} else if ($method === self::METHOD_REPLY) {
413
			$template->setSubject('Re: ' . $summary);
414
			$template->addHeading($l10n->t('Invitation updated'), $l10n->t('Hello %s,', [$attendeeName]));
415
			$template->addBodyText($l10n->t('The meeting »%s« with %s was updated.', [$summary, $inviteeName]));
416
		} else {
417
			$template->setSubject('Invitation: ' . $summary);
418
			$template->addHeading($l10n->t('%s invited you to »%s«', [$inviteeName, $summary]), $l10n->t('Hello %s,', [$attendeeName]));
419
		}
420
421
	}
422
423
	/**
424
	 * @param IEMailTemplate $template
425
	 * @param IL10N $l10n
426
	 * @param string $time
427
	 * @param string $location
428
	 * @param string $description
429
	 * @param string $url
430
	 */
431
	private function addBulletList(IEMailTemplate $template, IL10N $l10n, $time, $location, $description, $url) {
432
		$template->addBodyListItem($time, $l10n->t('When:'),
433
			$this->getAbsoluteImagePath('filetypes/text-calendar.svg'));
434
435
		if ($location) {
436
			$template->addBodyListItem($location, $l10n->t('Where:'),
437
				$this->getAbsoluteImagePath('filetypes/location.svg'));
438
		}
439
		if ($description) {
440
			$template->addBodyListItem((string)$description, $l10n->t('Description:'),
441
				$this->getAbsoluteImagePath('filetypes/text.svg'));
442
		}
443
		if ($url) {
444
			$template->addBodyListItem((string)$url, $l10n->t('Link:'),
445
				$this->getAbsoluteImagePath('filetypes/link.svg'));
446
		}
447
	}
448
449
	/**
450
	 * @param string $path
451
	 * @return string
452
	 */
453
	private function getAbsoluteImagePath($path) {
454
		return $this->urlGenerator->getAbsoluteURL(
455
			$this->urlGenerator->imagePath('core', $path)
456
		);
457
	}
458
}
459