1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* @copyright Copyright (c) 2019, Thomas Citharel |
7
|
|
|
* @copyright Copyright (c) 2019, Georg Ehrke |
8
|
|
|
* |
9
|
|
|
* @author Christoph Wurst <[email protected]> |
10
|
|
|
* @author Georg Ehrke <[email protected]> |
11
|
|
|
* @author Joas Schilling <[email protected]> |
12
|
|
|
* @author Richard Steinmetz <[email protected]> |
13
|
|
|
* @author Roeland Jago Douma <[email protected]> |
14
|
|
|
* @author Thomas Citharel <[email protected]> |
15
|
|
|
* |
16
|
|
|
* @license GNU AGPL version 3 or any later version |
17
|
|
|
* |
18
|
|
|
* This program is free software: you can redistribute it and/or modify |
19
|
|
|
* it under the terms of the GNU Affero General Public License as |
20
|
|
|
* published by the Free Software Foundation, either version 3 of the |
21
|
|
|
* License, or (at your option) any later version. |
22
|
|
|
* |
23
|
|
|
* This program is distributed in the hope that it will be useful, |
24
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
25
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
26
|
|
|
* GNU Affero General Public License for more details. |
27
|
|
|
* |
28
|
|
|
* You should have received a copy of the GNU Affero General Public License |
29
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
30
|
|
|
* |
31
|
|
|
*/ |
32
|
|
|
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; |
33
|
|
|
|
34
|
|
|
use DateTime; |
35
|
|
|
use OCP\IConfig; |
36
|
|
|
use OCP\IL10N; |
37
|
|
|
use OCP\IURLGenerator; |
38
|
|
|
use OCP\IUser; |
39
|
|
|
use OCP\L10N\IFactory as L10NFactory; |
40
|
|
|
use OCP\Mail\IEMailTemplate; |
41
|
|
|
use OCP\Mail\IMailer; |
42
|
|
|
use Psr\Log\LoggerInterface; |
43
|
|
|
use Sabre\VObject; |
44
|
|
|
use Sabre\VObject\Component\VEvent; |
45
|
|
|
use Sabre\VObject\Parameter; |
46
|
|
|
use Sabre\VObject\Property; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Class EmailProvider |
50
|
|
|
* |
51
|
|
|
* @package OCA\DAV\CalDAV\Reminder\NotificationProvider |
52
|
|
|
*/ |
53
|
|
|
class EmailProvider extends AbstractProvider { |
54
|
|
|
/** @var string */ |
55
|
|
|
public const NOTIFICATION_TYPE = 'EMAIL'; |
56
|
|
|
|
57
|
|
|
private IMailer $mailer; |
58
|
|
|
|
59
|
|
|
public function __construct(IConfig $config, |
60
|
|
|
IMailer $mailer, |
61
|
|
|
LoggerInterface $logger, |
62
|
|
|
L10NFactory $l10nFactory, |
63
|
|
|
IURLGenerator $urlGenerator) { |
64
|
|
|
parent::__construct($logger, $l10nFactory, $urlGenerator, $config); |
65
|
|
|
$this->mailer = $mailer; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Send out notification via email |
70
|
|
|
* |
71
|
|
|
* @param VEvent $vevent |
72
|
|
|
* @param string $calendarDisplayName |
73
|
|
|
* @param string[] $principalEmailAddresses |
74
|
|
|
* @param array $users |
75
|
|
|
* @throws \Exception |
76
|
|
|
*/ |
77
|
|
|
public function send(VEvent $vevent, |
78
|
|
|
string $calendarDisplayName, |
79
|
|
|
array $principalEmailAddresses, |
80
|
|
|
array $users = []):void { |
81
|
|
|
$fallbackLanguage = $this->getFallbackLanguage(); |
82
|
|
|
|
83
|
|
|
$organizerEmailAddress = null; |
84
|
|
|
if (isset($vevent->ORGANIZER)) { |
85
|
|
|
$organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
$emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users); |
89
|
|
|
$emailAddressesOfAttendees = []; |
90
|
|
|
if (count($principalEmailAddresses) === 0 |
91
|
|
|
|| ($organizerEmailAddress && in_array($organizerEmailAddress, $principalEmailAddresses, true)) |
92
|
|
|
) { |
93
|
|
|
$emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
// Quote from php.net: |
97
|
|
|
// If the input arrays have the same string keys, then the later value for that key will overwrite the previous one. |
98
|
|
|
// => if there are duplicate email addresses, it will always take the system value |
99
|
|
|
$emailAddresses = array_merge( |
100
|
|
|
$emailAddressesOfAttendees, |
101
|
|
|
$emailAddressesOfSharees |
102
|
|
|
); |
103
|
|
|
|
104
|
|
|
$sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage); |
105
|
|
|
$organizer = $this->getOrganizerEMailAndNameFromEvent($vevent); |
106
|
|
|
|
107
|
|
|
foreach ($sortedByLanguage as $lang => $emailAddresses) { |
108
|
|
|
if (!$this->hasL10NForLang($lang)) { |
109
|
|
|
$lang = $fallbackLanguage; |
110
|
|
|
} |
111
|
|
|
$l10n = $this->getL10NForLang($lang); |
112
|
|
|
$fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply'); |
113
|
|
|
|
114
|
|
|
$template = $this->mailer->createEMailTemplate('dav.calendarReminder'); |
115
|
|
|
$template->addHeader(); |
116
|
|
|
$this->addSubjectAndHeading($template, $l10n, $vevent); |
117
|
|
|
$this->addBulletList($template, $l10n, $calendarDisplayName, $vevent); |
118
|
|
|
$template->addFooter(); |
119
|
|
|
|
120
|
|
|
foreach ($emailAddresses as $emailAddress) { |
121
|
|
|
if (!$this->mailer->validateMailAddress($emailAddress)) { |
122
|
|
|
$this->logger->error('Email address {address} for reminder notification is incorrect', ['app' => 'dav', 'address' => $emailAddress]); |
123
|
|
|
continue; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
$message = $this->mailer->createMessage(); |
127
|
|
|
$message->setFrom([$fromEMail]); |
128
|
|
|
if ($organizer) { |
129
|
|
|
$message->setReplyTo($organizer); |
130
|
|
|
} |
131
|
|
|
$message->setTo([$emailAddress]); |
132
|
|
|
$message->useTemplate($template); |
133
|
|
|
|
134
|
|
|
try { |
135
|
|
|
$failed = $this->mailer->send($message); |
136
|
|
|
if ($failed) { |
|
|
|
|
137
|
|
|
$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]); |
138
|
|
|
} |
139
|
|
|
} catch (\Exception $ex) { |
140
|
|
|
$this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]); |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* @param IEMailTemplate $template |
148
|
|
|
* @param IL10N $l10n |
149
|
|
|
* @param VEvent $vevent |
150
|
|
|
*/ |
151
|
|
|
private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void { |
152
|
|
|
$template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n)); |
153
|
|
|
$template->addHeading($this->getTitleFromVEvent($vevent, $l10n)); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* @param IEMailTemplate $template |
158
|
|
|
* @param IL10N $l10n |
159
|
|
|
* @param string $calendarDisplayName |
160
|
|
|
* @param array $eventData |
161
|
|
|
*/ |
162
|
|
|
private function addBulletList(IEMailTemplate $template, |
163
|
|
|
IL10N $l10n, |
164
|
|
|
string $calendarDisplayName, |
165
|
|
|
VEvent $vevent):void { |
166
|
|
|
$template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'), |
167
|
|
|
$this->getAbsoluteImagePath('actions/info.png')); |
168
|
|
|
|
169
|
|
|
$template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'), |
170
|
|
|
$this->getAbsoluteImagePath('places/calendar.png')); |
171
|
|
|
|
172
|
|
|
if (isset($vevent->LOCATION)) { |
173
|
|
|
$template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'), |
174
|
|
|
$this->getAbsoluteImagePath('actions/address.png')); |
175
|
|
|
} |
176
|
|
|
if (isset($vevent->DESCRIPTION)) { |
177
|
|
|
$template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'), |
178
|
|
|
$this->getAbsoluteImagePath('actions/more.png')); |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
private function getAbsoluteImagePath(string $path):string { |
183
|
|
|
return $this->urlGenerator->getAbsoluteURL( |
184
|
|
|
$this->urlGenerator->imagePath('core', $path) |
185
|
|
|
); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* @param VEvent $vevent |
190
|
|
|
* @return array|null |
191
|
|
|
*/ |
192
|
|
|
private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array { |
193
|
|
|
if (!$vevent->ORGANIZER) { |
194
|
|
|
return null; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
$organizer = $vevent->ORGANIZER; |
198
|
|
|
if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) { |
199
|
|
|
return null; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
$organizerEMail = substr($organizer->getValue(), 7); |
203
|
|
|
|
204
|
|
|
if (!$this->mailer->validateMailAddress($organizerEMail)) { |
205
|
|
|
return null; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
$name = $organizer->offsetGet('CN'); |
209
|
|
|
if ($name instanceof Parameter) { |
210
|
|
|
return [$organizerEMail => $name]; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
return [$organizerEMail]; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* @param array<string, array{LANG?: string}> $emails |
218
|
|
|
* @return array<string, string[]> |
219
|
|
|
*/ |
220
|
|
|
private function sortEMailAddressesByLanguage(array $emails, |
221
|
|
|
string $defaultLanguage):array { |
222
|
|
|
$sortedByLanguage = []; |
223
|
|
|
|
224
|
|
|
foreach ($emails as $emailAddress => $parameters) { |
225
|
|
|
if (isset($parameters['LANG'])) { |
226
|
|
|
$lang = $parameters['LANG']; |
227
|
|
|
} else { |
228
|
|
|
$lang = $defaultLanguage; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
if (!isset($sortedByLanguage[$lang])) { |
232
|
|
|
$sortedByLanguage[$lang] = []; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
$sortedByLanguage[$lang][] = $emailAddress; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
return $sortedByLanguage; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* @param VEvent $vevent |
243
|
|
|
* @return array<string, array{LANG?: string}> |
244
|
|
|
*/ |
245
|
|
|
private function getAllEMailAddressesFromEvent(VEvent $vevent):array { |
246
|
|
|
$emailAddresses = []; |
247
|
|
|
|
248
|
|
|
if (isset($vevent->ATTENDEE)) { |
249
|
|
|
foreach ($vevent->ATTENDEE as $attendee) { |
250
|
|
|
if (!($attendee instanceof VObject\Property)) { |
251
|
|
|
continue; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$cuType = $this->getCUTypeOfAttendee($attendee); |
255
|
|
|
if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) { |
256
|
|
|
// Don't send emails to things |
257
|
|
|
continue; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
$partstat = $this->getPartstatOfAttendee($attendee); |
261
|
|
|
if ($partstat === 'DECLINED') { |
262
|
|
|
// Don't send out emails to people who declined |
263
|
|
|
continue; |
264
|
|
|
} |
265
|
|
|
if ($partstat === 'DELEGATED') { |
266
|
|
|
$delegates = $attendee->offsetGet('DELEGATED-TO'); |
267
|
|
|
if (!($delegates instanceof VObject\Parameter)) { |
268
|
|
|
continue; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
$emailAddressesOfDelegates = $delegates->getParts(); |
272
|
|
|
foreach ($emailAddressesOfDelegates as $addressesOfDelegate) { |
273
|
|
|
if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) { |
274
|
|
|
$delegateEmail = substr($addressesOfDelegate, 7); |
275
|
|
|
if ($this->mailer->validateMailAddress($delegateEmail)) { |
276
|
|
|
$emailAddresses[$delegateEmail] = []; |
277
|
|
|
} |
278
|
|
|
} |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
continue; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
$emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee); |
285
|
|
|
if ($emailAddressOfAttendee !== null) { |
286
|
|
|
$properties = []; |
287
|
|
|
|
288
|
|
|
$langProp = $attendee->offsetGet('LANG'); |
289
|
|
|
if ($langProp instanceof VObject\Parameter && $langProp->getValue() !== null) { |
290
|
|
|
$properties['LANG'] = $langProp->getValue(); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
$emailAddresses[$emailAddressOfAttendee] = $properties; |
294
|
|
|
} |
295
|
|
|
} |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) { |
299
|
|
|
$organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER); |
300
|
|
|
if ($organizerEmailAddress !== null) { |
301
|
|
|
$emailAddresses[$organizerEmailAddress] = []; |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
return $emailAddresses; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
private function getCUTypeOfAttendee(VObject\Property $attendee):string { |
309
|
|
|
$cuType = $attendee->offsetGet('CUTYPE'); |
310
|
|
|
if ($cuType instanceof VObject\Parameter) { |
311
|
|
|
return strtoupper($cuType->getValue()); |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
return 'INDIVIDUAL'; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
private function getPartstatOfAttendee(VObject\Property $attendee):string { |
318
|
|
|
$partstat = $attendee->offsetGet('PARTSTAT'); |
319
|
|
|
if ($partstat instanceof VObject\Parameter) { |
320
|
|
|
return strtoupper($partstat->getValue()); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
return 'NEEDS-ACTION'; |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
private function hasAttendeeMailURI(VObject\Property $attendee): bool { |
327
|
|
|
return stripos($attendee->getValue(), 'mailto:') === 0; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
private function getEMailAddressOfAttendee(VObject\Property $attendee): ?string { |
331
|
|
|
if (!$this->hasAttendeeMailURI($attendee)) { |
332
|
|
|
return null; |
333
|
|
|
} |
334
|
|
|
$attendeeEMail = substr($attendee->getValue(), 7); |
335
|
|
|
if (!$this->mailer->validateMailAddress($attendeeEMail)) { |
336
|
|
|
return null; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
return $attendeeEMail; |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* @param IUser[] $users |
344
|
|
|
* @return array<string, array{LANG?: string}> |
345
|
|
|
*/ |
346
|
|
|
private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array { |
347
|
|
|
$emailAddresses = []; |
348
|
|
|
|
349
|
|
|
foreach ($users as $user) { |
350
|
|
|
$emailAddress = $user->getEMailAddress(); |
351
|
|
|
if ($emailAddress) { |
352
|
|
|
$lang = $this->l10nFactory->getUserLanguage($user); |
353
|
|
|
if ($lang) { |
354
|
|
|
$emailAddresses[$emailAddress] = [ |
355
|
|
|
'LANG' => $lang, |
356
|
|
|
]; |
357
|
|
|
} else { |
358
|
|
|
$emailAddresses[$emailAddress] = []; |
359
|
|
|
} |
360
|
|
|
} |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
return $emailAddresses; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* @throws \Exception |
368
|
|
|
*/ |
369
|
|
|
private function generateDateString(IL10N $l10n, VEvent $vevent): string { |
370
|
|
|
$isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date; |
371
|
|
|
|
372
|
|
|
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */ |
373
|
|
|
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */ |
374
|
|
|
/** @var \DateTimeImmutable $dtstartDt */ |
375
|
|
|
$dtstartDt = $vevent->DTSTART->getDateTime(); |
|
|
|
|
376
|
|
|
/** @var \DateTimeImmutable $dtendDt */ |
377
|
|
|
$dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime(); |
378
|
|
|
|
379
|
|
|
$diff = $dtstartDt->diff($dtendDt); |
380
|
|
|
|
381
|
|
|
$dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM)); |
382
|
|
|
$dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM)); |
383
|
|
|
|
384
|
|
|
if ($isAllDay) { |
385
|
|
|
// One day event |
386
|
|
|
if ($diff->days === 1) { |
387
|
|
|
return $this->getDateString($l10n, $dtstartDt); |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
return implode(' - ', [ |
391
|
|
|
$this->getDateString($l10n, $dtstartDt), |
392
|
|
|
$this->getDateString($l10n, $dtendDt), |
393
|
|
|
]); |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
$startTimezone = $endTimezone = null; |
397
|
|
|
if (!$vevent->DTSTART->isFloating()) { |
|
|
|
|
398
|
|
|
$startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName(); |
399
|
|
|
$endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName(); |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
$localeStart = implode(', ', [ |
403
|
|
|
$this->getWeekDayName($l10n, $dtstartDt), |
404
|
|
|
$this->getDateTimeString($l10n, $dtstartDt) |
405
|
|
|
]); |
406
|
|
|
|
407
|
|
|
// always show full date with timezone if timezones are different |
408
|
|
|
if ($startTimezone !== $endTimezone) { |
409
|
|
|
$localeEnd = implode(', ', [ |
410
|
|
|
$this->getWeekDayName($l10n, $dtendDt), |
411
|
|
|
$this->getDateTimeString($l10n, $dtendDt) |
412
|
|
|
]); |
413
|
|
|
|
414
|
|
|
return $localeStart |
415
|
|
|
. ' (' . $startTimezone . ') ' |
416
|
|
|
. ' - ' |
417
|
|
|
. $localeEnd |
418
|
|
|
. ' (' . $endTimezone . ')'; |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
// Show only the time if the day is the same |
422
|
|
|
$localeEnd = $this->isDayEqual($dtstartDt, $dtendDt) |
423
|
|
|
? $this->getTimeString($l10n, $dtendDt) |
424
|
|
|
: implode(', ', [ |
425
|
|
|
$this->getWeekDayName($l10n, $dtendDt), |
426
|
|
|
$this->getDateTimeString($l10n, $dtendDt) |
427
|
|
|
]); |
428
|
|
|
|
429
|
|
|
return $localeStart |
430
|
|
|
. ' - ' |
431
|
|
|
. $localeEnd |
432
|
|
|
. ' (' . $startTimezone . ')'; |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
private function isDayEqual(DateTime $dtStart, |
436
|
|
|
DateTime $dtEnd):bool { |
437
|
|
|
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d'); |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
private function getWeekDayName(IL10N $l10n, DateTime $dt):string { |
441
|
|
|
return (string)$l10n->l('weekdayName', $dt, ['width' => 'abbreviated']); |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
private function getDateString(IL10N $l10n, DateTime $dt):string { |
445
|
|
|
return (string)$l10n->l('date', $dt, ['width' => 'medium']); |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
private function getDateTimeString(IL10N $l10n, DateTime $dt):string { |
449
|
|
|
return (string)$l10n->l('datetime', $dt, ['width' => 'medium|short']); |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
private function getTimeString(IL10N $l10n, DateTime $dt):string { |
453
|
|
|
return (string)$l10n->l('time', $dt, ['width' => 'short']); |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string { |
457
|
|
|
if (isset($vevent->SUMMARY)) { |
458
|
|
|
return (string)$vevent->SUMMARY; |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
return $l10n->t('Untitled event'); |
462
|
|
|
} |
463
|
|
|
} |
464
|
|
|
|
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.