Passed
Push — master ( 9cef2c...0459f5 )
by John
14:23 queued 13s
created
apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php 1 patch
Indentation   +409 added lines, -409 removed lines patch added patch discarded remove patch
@@ -51,413 +51,413 @@
 block discarded – undo
51 51
  * @package OCA\DAV\CalDAV\Reminder\NotificationProvider
52 52
  */
53 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
-	}
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 463
 }
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Schedule/IMipPlugin.php 2 patches
Indentation   +614 added lines, -614 removed lines patch added patch discarded remove patch
@@ -71,171 +71,171 @@  discard block
 block discarded – undo
71 71
  * @license http://sabre.io/license/ Modified BSD License
72 72
  */
73 73
 class IMipPlugin extends SabreIMipPlugin {
74
-	/** @var string */
75
-	private $userId;
76
-
77
-	/** @var IConfig */
78
-	private $config;
79
-
80
-	/** @var IMailer */
81
-	private $mailer;
82
-
83
-	private LoggerInterface $logger;
84
-
85
-	/** @var ITimeFactory */
86
-	private $timeFactory;
87
-
88
-	/** @var L10NFactory */
89
-	private $l10nFactory;
90
-
91
-	/** @var IURLGenerator */
92
-	private $urlGenerator;
93
-
94
-	/** @var ISecureRandom */
95
-	private $random;
96
-
97
-	/** @var IDBConnection */
98
-	private $db;
99
-
100
-	/** @var Defaults */
101
-	private $defaults;
102
-
103
-	/** @var IUserManager */
104
-	private $userManager;
105
-
106
-	public const MAX_DATE = '2038-01-01';
107
-
108
-	public const METHOD_REQUEST = 'request';
109
-	public const METHOD_REPLY = 'reply';
110
-	public const METHOD_CANCEL = 'cancel';
111
-	public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages
112
-
113
-	public function __construct(IConfig $config, IMailer $mailer,
114
-								LoggerInterface $logger,
115
-								ITimeFactory $timeFactory, L10NFactory $l10nFactory,
116
-								IURLGenerator $urlGenerator, Defaults $defaults,
117
-								ISecureRandom $random, IDBConnection $db, IUserManager $userManager,
118
-								$userId) {
119
-		parent::__construct('');
120
-		$this->userId = $userId;
121
-		$this->config = $config;
122
-		$this->mailer = $mailer;
123
-		$this->logger = $logger;
124
-		$this->timeFactory = $timeFactory;
125
-		$this->l10nFactory = $l10nFactory;
126
-		$this->urlGenerator = $urlGenerator;
127
-		$this->random = $random;
128
-		$this->db = $db;
129
-		$this->defaults = $defaults;
130
-		$this->userManager = $userManager;
131
-	}
132
-
133
-	/**
134
-	 * Event handler for the 'schedule' event.
135
-	 *
136
-	 * @param Message $iTipMessage
137
-	 * @return void
138
-	 */
139
-	public function schedule(Message $iTipMessage) {
140
-		// Not sending any emails if the system considers the update
141
-		// insignificant.
142
-		if (!$iTipMessage->significantChange) {
143
-			if (!$iTipMessage->scheduleStatus) {
144
-				$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
145
-			}
146
-			return;
147
-		}
148
-
149
-		$summary = $iTipMessage->message->VEVENT->SUMMARY;
150
-
151
-		if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') {
152
-			return;
153
-		}
154
-
155
-		if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
156
-			return;
157
-		}
158
-
159
-		// don't send out mails for events that already took place
160
-		$lastOccurrence = $this->getLastOccurrence($iTipMessage->message);
161
-		$currentTime = $this->timeFactory->getTime();
162
-		if ($lastOccurrence < $currentTime) {
163
-			return;
164
-		}
165
-
166
-		// Strip off mailto:
167
-		$sender = substr($iTipMessage->sender, 7);
168
-		$recipient = substr($iTipMessage->recipient, 7);
169
-		if (!$this->mailer->validateMailAddress($recipient)) {
170
-			// Nothing to send if the recipient doesn't have a valid email address
171
-			$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
172
-			return;
173
-		}
174
-
175
-		$senderName = $iTipMessage->senderName ?: null;
176
-		$recipientName = $iTipMessage->recipientName ?: null;
177
-
178
-		if ($senderName === null || empty(trim($senderName))) {
179
-			$senderName = $this->userManager->getDisplayName($this->userId);
180
-		}
181
-
182
-		/** @var VEvent $vevent */
183
-		$vevent = $iTipMessage->message->VEVENT;
184
-
185
-		$attendee = $this->getCurrentAttendee($iTipMessage);
186
-		$defaultLang = $this->l10nFactory->findGenericLanguage();
187
-		$lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee);
188
-		$l10n = $this->l10nFactory->get('dav', $lang);
189
-
190
-		$meetingAttendeeName = $recipientName ?: $recipient;
191
-		$meetingInviteeName = $senderName ?: $sender;
192
-
193
-		$meetingTitle = $vevent->SUMMARY;
194
-		$meetingDescription = $vevent->DESCRIPTION;
195
-
196
-
197
-		$meetingUrl = $vevent->URL;
198
-		$meetingLocation = $vevent->LOCATION;
199
-
200
-		$defaultVal = '--';
201
-
202
-		$method = self::METHOD_REQUEST;
203
-		switch (strtolower($iTipMessage->method)) {
204
-			case self::METHOD_REPLY:
205
-				$method = self::METHOD_REPLY;
206
-				break;
207
-			case self::METHOD_CANCEL:
208
-				$method = self::METHOD_CANCEL;
209
-				break;
210
-		}
211
-
212
-		$data = [
213
-			'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
214
-			'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
215
-			'meeting_title' => (string)$meetingTitle ?: $defaultVal,
216
-			'meeting_description' => (string)$meetingDescription ?: $defaultVal,
217
-			'meeting_url' => (string)$meetingUrl ?: $defaultVal,
218
-		];
219
-
220
-		$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
221
-		$fromName = $l10n->t('%1$s via %2$s', [$senderName ?? $this->userId, $this->defaults->getName()]);
222
-
223
-		$message = $this->mailer->createMessage()
224
-			->setFrom([$fromEMail => $fromName])
225
-			->setTo([$recipient => $recipientName])
226
-			->setReplyTo([$sender => $senderName]);
227
-
228
-		$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
229
-		$template->addHeader();
230
-
231
-		$summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
232
-
233
-		$this->addSubjectAndHeading($template, $l10n, $method, $summary);
234
-		$this->addBulletList($template, $l10n, $vevent);
235
-
236
-		// Only add response buttons to invitation requests: Fix Issue #11230
237
-		if (($method == self::METHOD_REQUEST) && $this->getAttendeeRsvpOrReqForParticipant($attendee)) {
238
-			/*
74
+    /** @var string */
75
+    private $userId;
76
+
77
+    /** @var IConfig */
78
+    private $config;
79
+
80
+    /** @var IMailer */
81
+    private $mailer;
82
+
83
+    private LoggerInterface $logger;
84
+
85
+    /** @var ITimeFactory */
86
+    private $timeFactory;
87
+
88
+    /** @var L10NFactory */
89
+    private $l10nFactory;
90
+
91
+    /** @var IURLGenerator */
92
+    private $urlGenerator;
93
+
94
+    /** @var ISecureRandom */
95
+    private $random;
96
+
97
+    /** @var IDBConnection */
98
+    private $db;
99
+
100
+    /** @var Defaults */
101
+    private $defaults;
102
+
103
+    /** @var IUserManager */
104
+    private $userManager;
105
+
106
+    public const MAX_DATE = '2038-01-01';
107
+
108
+    public const METHOD_REQUEST = 'request';
109
+    public const METHOD_REPLY = 'reply';
110
+    public const METHOD_CANCEL = 'cancel';
111
+    public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages
112
+
113
+    public function __construct(IConfig $config, IMailer $mailer,
114
+                                LoggerInterface $logger,
115
+                                ITimeFactory $timeFactory, L10NFactory $l10nFactory,
116
+                                IURLGenerator $urlGenerator, Defaults $defaults,
117
+                                ISecureRandom $random, IDBConnection $db, IUserManager $userManager,
118
+                                $userId) {
119
+        parent::__construct('');
120
+        $this->userId = $userId;
121
+        $this->config = $config;
122
+        $this->mailer = $mailer;
123
+        $this->logger = $logger;
124
+        $this->timeFactory = $timeFactory;
125
+        $this->l10nFactory = $l10nFactory;
126
+        $this->urlGenerator = $urlGenerator;
127
+        $this->random = $random;
128
+        $this->db = $db;
129
+        $this->defaults = $defaults;
130
+        $this->userManager = $userManager;
131
+    }
132
+
133
+    /**
134
+     * Event handler for the 'schedule' event.
135
+     *
136
+     * @param Message $iTipMessage
137
+     * @return void
138
+     */
139
+    public function schedule(Message $iTipMessage) {
140
+        // Not sending any emails if the system considers the update
141
+        // insignificant.
142
+        if (!$iTipMessage->significantChange) {
143
+            if (!$iTipMessage->scheduleStatus) {
144
+                $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
145
+            }
146
+            return;
147
+        }
148
+
149
+        $summary = $iTipMessage->message->VEVENT->SUMMARY;
150
+
151
+        if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') {
152
+            return;
153
+        }
154
+
155
+        if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
156
+            return;
157
+        }
158
+
159
+        // don't send out mails for events that already took place
160
+        $lastOccurrence = $this->getLastOccurrence($iTipMessage->message);
161
+        $currentTime = $this->timeFactory->getTime();
162
+        if ($lastOccurrence < $currentTime) {
163
+            return;
164
+        }
165
+
166
+        // Strip off mailto:
167
+        $sender = substr($iTipMessage->sender, 7);
168
+        $recipient = substr($iTipMessage->recipient, 7);
169
+        if (!$this->mailer->validateMailAddress($recipient)) {
170
+            // Nothing to send if the recipient doesn't have a valid email address
171
+            $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
172
+            return;
173
+        }
174
+
175
+        $senderName = $iTipMessage->senderName ?: null;
176
+        $recipientName = $iTipMessage->recipientName ?: null;
177
+
178
+        if ($senderName === null || empty(trim($senderName))) {
179
+            $senderName = $this->userManager->getDisplayName($this->userId);
180
+        }
181
+
182
+        /** @var VEvent $vevent */
183
+        $vevent = $iTipMessage->message->VEVENT;
184
+
185
+        $attendee = $this->getCurrentAttendee($iTipMessage);
186
+        $defaultLang = $this->l10nFactory->findGenericLanguage();
187
+        $lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee);
188
+        $l10n = $this->l10nFactory->get('dav', $lang);
189
+
190
+        $meetingAttendeeName = $recipientName ?: $recipient;
191
+        $meetingInviteeName = $senderName ?: $sender;
192
+
193
+        $meetingTitle = $vevent->SUMMARY;
194
+        $meetingDescription = $vevent->DESCRIPTION;
195
+
196
+
197
+        $meetingUrl = $vevent->URL;
198
+        $meetingLocation = $vevent->LOCATION;
199
+
200
+        $defaultVal = '--';
201
+
202
+        $method = self::METHOD_REQUEST;
203
+        switch (strtolower($iTipMessage->method)) {
204
+            case self::METHOD_REPLY:
205
+                $method = self::METHOD_REPLY;
206
+                break;
207
+            case self::METHOD_CANCEL:
208
+                $method = self::METHOD_CANCEL;
209
+                break;
210
+        }
211
+
212
+        $data = [
213
+            'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
214
+            'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
215
+            'meeting_title' => (string)$meetingTitle ?: $defaultVal,
216
+            'meeting_description' => (string)$meetingDescription ?: $defaultVal,
217
+            'meeting_url' => (string)$meetingUrl ?: $defaultVal,
218
+        ];
219
+
220
+        $fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
221
+        $fromName = $l10n->t('%1$s via %2$s', [$senderName ?? $this->userId, $this->defaults->getName()]);
222
+
223
+        $message = $this->mailer->createMessage()
224
+            ->setFrom([$fromEMail => $fromName])
225
+            ->setTo([$recipient => $recipientName])
226
+            ->setReplyTo([$sender => $senderName]);
227
+
228
+        $template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
229
+        $template->addHeader();
230
+
231
+        $summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
232
+
233
+        $this->addSubjectAndHeading($template, $l10n, $method, $summary);
234
+        $this->addBulletList($template, $l10n, $vevent);
235
+
236
+        // Only add response buttons to invitation requests: Fix Issue #11230
237
+        if (($method == self::METHOD_REQUEST) && $this->getAttendeeRsvpOrReqForParticipant($attendee)) {
238
+            /*
239 239
 			** Only offer invitation accept/reject buttons, which link back to the
240 240
 			** nextcloud server, to recipients who can access the nextcloud server via
241 241
 			** their internet/intranet.  Issue #12156
@@ -254,453 +254,453 @@  discard block
 block discarded – undo
254 254
 			** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
255 255
 			*/
256 256
 
257
-			$recipientDomain = substr(strrchr($recipient, "@"), 1);
258
-			$invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
259
-
260
-			if (strcmp('yes', $invitationLinkRecipients[0]) === 0
261
-				 || in_array(strtolower($recipient), $invitationLinkRecipients)
262
-				 || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
263
-				$this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence);
264
-			}
265
-		}
266
-
267
-		$template->addFooter();
268
-
269
-		$message->useTemplate($template);
270
-
271
-		$attachment = $this->mailer->createAttachment(
272
-			$iTipMessage->message->serialize(),
273
-			'event.ics',// TODO(leon): Make file name unique, e.g. add event id
274
-			'text/calendar; method=' . $iTipMessage->method
275
-		);
276
-		$message->attach($attachment);
277
-
278
-		try {
279
-			$failed = $this->mailer->send($message);
280
-			$iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
281
-			if ($failed) {
282
-				$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
283
-				$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
284
-			}
285
-		} catch (\Exception $ex) {
286
-			$this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]);
287
-			$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
288
-		}
289
-	}
290
-
291
-	/**
292
-	 * check if event took place in the past already
293
-	 * @param VCalendar $vObject
294
-	 * @return int
295
-	 */
296
-	private function getLastOccurrence(VCalendar $vObject) {
297
-		/** @var VEvent $component */
298
-		$component = $vObject->VEVENT;
299
-
300
-		$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
301
-		// Finding the last occurrence is a bit harder
302
-		if (!isset($component->RRULE)) {
303
-			if (isset($component->DTEND)) {
304
-				$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
305
-			} elseif (isset($component->DURATION)) {
306
-				/** @var \DateTime $endDate */
307
-				$endDate = clone $component->DTSTART->getDateTime();
308
-				// $component->DTEND->getDateTime() returns DateTimeImmutable
309
-				$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
310
-				$lastOccurrence = $endDate->getTimestamp();
311
-			} elseif (!$component->DTSTART->hasTime()) {
312
-				/** @var \DateTime $endDate */
313
-				$endDate = clone $component->DTSTART->getDateTime();
314
-				// $component->DTSTART->getDateTime() returns DateTimeImmutable
315
-				$endDate = $endDate->modify('+1 day');
316
-				$lastOccurrence = $endDate->getTimestamp();
317
-			} else {
318
-				$lastOccurrence = $firstOccurrence;
319
-			}
320
-		} else {
321
-			$it = new EventIterator($vObject, (string)$component->UID);
322
-			$maxDate = new \DateTime(self::MAX_DATE);
323
-			if ($it->isInfinite()) {
324
-				$lastOccurrence = $maxDate->getTimestamp();
325
-			} else {
326
-				$end = $it->getDtEnd();
327
-				while ($it->valid() && $end < $maxDate) {
328
-					$end = $it->getDtEnd();
329
-					$it->next();
330
-				}
331
-				$lastOccurrence = $end->getTimestamp();
332
-			}
333
-		}
334
-
335
-		return $lastOccurrence;
336
-	}
337
-
338
-	/**
339
-	 * @param Message $iTipMessage
340
-	 * @return null|Property
341
-	 */
342
-	private function getCurrentAttendee(Message $iTipMessage) {
343
-		/** @var VEvent $vevent */
344
-		$vevent = $iTipMessage->message->VEVENT;
345
-		$attendees = $vevent->select('ATTENDEE');
346
-		foreach ($attendees as $attendee) {
347
-			/** @var Property $attendee */
348
-			if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
349
-				return $attendee;
350
-			}
351
-		}
352
-		return null;
353
-	}
354
-
355
-	/**
356
-	 * @param string $default
357
-	 * @param Property|null $attendee
358
-	 * @return string
359
-	 */
360
-	private function getAttendeeLangOrDefault($default, Property $attendee = null) {
361
-		if ($attendee !== null) {
362
-			$lang = $attendee->offsetGet('LANGUAGE');
363
-			if ($lang instanceof Parameter) {
364
-				return $lang->getValue();
365
-			}
366
-		}
367
-		return $default;
368
-	}
369
-
370
-	/**
371
-	 * @param Property|null $attendee
372
-	 * @return bool
373
-	 */
374
-	private function getAttendeeRsvpOrReqForParticipant(Property $attendee = null) {
375
-		if ($attendee !== null) {
376
-			$rsvp = $attendee->offsetGet('RSVP');
377
-			if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
378
-				return true;
379
-			}
380
-			$role = $attendee->offsetGet('ROLE');
381
-			// @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16
382
-			// Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set
383
-			if ($role === null
384
-				|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0))
385
-				|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0))
386
-			) {
387
-				return true;
388
-			}
389
-		}
390
-		// RFC 5545 3.2.17: default RSVP is false
391
-		return false;
392
-	}
393
-
394
-	/**
395
-	 * @param IL10N $l10n
396
-	 * @param VEvent $vevent
397
-	 */
398
-	private function generateWhenString(IL10N $l10n, VEvent $vevent) {
399
-		$dtstart = $vevent->DTSTART;
400
-		if (isset($vevent->DTEND)) {
401
-			$dtend = $vevent->DTEND;
402
-		} elseif (isset($vevent->DURATION)) {
403
-			$isFloating = $vevent->DTSTART->isFloating();
404
-			$dtend = clone $vevent->DTSTART;
405
-			$endDateTime = $dtend->getDateTime();
406
-			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
407
-			$dtend->setDateTime($endDateTime, $isFloating);
408
-		} elseif (!$vevent->DTSTART->hasTime()) {
409
-			$isFloating = $vevent->DTSTART->isFloating();
410
-			$dtend = clone $vevent->DTSTART;
411
-			$endDateTime = $dtend->getDateTime();
412
-			$endDateTime = $endDateTime->modify('+1 day');
413
-			$dtend->setDateTime($endDateTime, $isFloating);
414
-		} else {
415
-			$dtend = clone $vevent->DTSTART;
416
-		}
417
-
418
-		$isAllDay = $dtstart instanceof Property\ICalendar\Date;
419
-
420
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
421
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
422
-		/** @var \DateTimeImmutable $dtstartDt */
423
-		$dtstartDt = $dtstart->getDateTime();
424
-		/** @var \DateTimeImmutable $dtendDt */
425
-		$dtendDt = $dtend->getDateTime();
426
-
427
-		$diff = $dtstartDt->diff($dtendDt);
428
-
429
-		$dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
430
-		$dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
431
-
432
-		if ($isAllDay) {
433
-			// One day event
434
-			if ($diff->days === 1) {
435
-				return $l10n->l('date', $dtstartDt, ['width' => 'medium']);
436
-			}
437
-
438
-			// DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
439
-			// the email should show 2020-01-01 to 2020-01-04.
440
-			$dtendDt->modify('-1 day');
441
-
442
-			//event that spans over multiple days
443
-			$localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
444
-			$localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
445
-
446
-			return $localeStart . ' - ' . $localeEnd;
447
-		}
448
-
449
-		/** @var Property\ICalendar\DateTime $dtstart */
450
-		/** @var Property\ICalendar\DateTime $dtend */
451
-		$isFloating = $dtstart->isFloating();
452
-		$startTimezone = $endTimezone = null;
453
-		if (!$isFloating) {
454
-			$prop = $dtstart->offsetGet('TZID');
455
-			if ($prop instanceof Parameter) {
456
-				$startTimezone = $prop->getValue();
457
-			}
458
-
459
-			$prop = $dtend->offsetGet('TZID');
460
-			if ($prop instanceof Parameter) {
461
-				$endTimezone = $prop->getValue();
462
-			}
463
-		}
464
-
465
-		$localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
466
-			$l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
467
-
468
-		// always show full date with timezone if timezones are different
469
-		if ($startTimezone !== $endTimezone) {
470
-			$localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
471
-
472
-			return $localeStart . ' (' . $startTimezone . ') - ' .
473
-				$localeEnd . ' (' . $endTimezone . ')';
474
-		}
475
-
476
-		// show only end time if date is the same
477
-		if ($this->isDayEqual($dtstartDt, $dtendDt)) {
478
-			$localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
479
-		} else {
480
-			$localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
481
-				$l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
482
-		}
483
-
484
-		return  $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
485
-	}
486
-
487
-	/**
488
-	 * @param \DateTime $dtStart
489
-	 * @param \DateTime $dtEnd
490
-	 * @return bool
491
-	 */
492
-	private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) {
493
-		return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
494
-	}
495
-
496
-	/**
497
-	 * @param IEMailTemplate $template
498
-	 * @param IL10N $l10n
499
-	 * @param string $method
500
-	 * @param string $summary
501
-	 */
502
-	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
503
-										  $method, $summary) {
504
-		if ($method === self::METHOD_CANCEL) {
505
-			// TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}"
506
-			$template->setSubject($l10n->t('Cancelled: %1$s', [$summary]));
507
-			$template->addHeading($l10n->t('Invitation canceled'));
508
-		} elseif ($method === self::METHOD_REPLY) {
509
-			// TRANSLATORS Subject for email, when an invitation is updated. Ex: "Re: {{Event Name}}"
510
-			$template->setSubject($l10n->t('Re: %1$s', [$summary]));
511
-			$template->addHeading($l10n->t('Invitation updated'));
512
-		} else {
513
-			// TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}"
514
-			$template->setSubject($l10n->t('Invitation: %1$s', [$summary]));
515
-			$template->addHeading($l10n->t('Invitation'));
516
-		}
517
-	}
518
-
519
-	/**
520
-	 * @param IEMailTemplate $template
521
-	 * @param IL10N $l10n
522
-	 * @param VEVENT $vevent
523
-	 */
524
-	private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) {
525
-		if ($vevent->SUMMARY) {
526
-			$template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'),
527
-				$this->getAbsoluteImagePath('caldav/title.png'), '', '', self::IMIP_INDENT);
528
-		}
529
-		$meetingWhen = $this->generateWhenString($l10n, $vevent);
530
-		if ($meetingWhen) {
531
-			$template->addBodyListItem($meetingWhen, $l10n->t('Time:'),
532
-				$this->getAbsoluteImagePath('caldav/time.png'), '', '', self::IMIP_INDENT);
533
-		}
534
-		if ($vevent->LOCATION) {
535
-			$template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'),
536
-				$this->getAbsoluteImagePath('caldav/location.png'), '', '', self::IMIP_INDENT);
537
-		}
538
-		if ($vevent->URL) {
539
-			$url = $vevent->URL->getValue();
540
-			$template->addBodyListItem(sprintf('<a href="%s">%s</a>',
541
-				htmlspecialchars($url),
542
-				htmlspecialchars($url)),
543
-				$l10n->t('Link:'),
544
-				$this->getAbsoluteImagePath('caldav/link.png'),
545
-				$url, '', self::IMIP_INDENT);
546
-		}
547
-
548
-		$this->addAttendees($template, $l10n, $vevent);
549
-
550
-		/* Put description last, like an email body, since it can be arbitrarily long */
551
-		if ($vevent->DESCRIPTION) {
552
-			$template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'),
553
-				$this->getAbsoluteImagePath('caldav/description.png'), '', '', self::IMIP_INDENT);
554
-		}
555
-	}
556
-
557
-	/**
558
-	 * addAttendees: add organizer and attendee names/emails to iMip mail.
559
-	 *
560
-	 * Enable with DAV setting: invitation_list_attendees (default: no)
561
-	 *
562
-	 * The default is 'no', which matches old behavior, and is privacy preserving.
563
-	 *
564
-	 * To enable including attendees in invitation emails:
565
-	 *   % php occ config:app:set dav invitation_list_attendees --value yes
566
-	 *
567
-	 * @param IEMailTemplate $template
568
-	 * @param IL10N $l10n
569
-	 * @param Message $iTipMessage
570
-	 * @param int $lastOccurrence
571
-	 * @author brad2014 on github.com
572
-	 */
573
-
574
-	private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) {
575
-		if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
576
-			return;
577
-		}
578
-
579
-		if (isset($vevent->ORGANIZER)) {
580
-			/** @var Property\ICalendar\CalAddress $organizer */
581
-			$organizer = $vevent->ORGANIZER;
582
-			$organizerURI = $organizer->getNormalizedValue();
583
-			[$scheme,$organizerEmail] = explode(':', $organizerURI, 2); # strip off scheme mailto:
584
-			/** @var string|null $organizerName */
585
-			$organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
586
-			$organizerHTML = sprintf('<a href="%s">%s</a>',
587
-				htmlspecialchars($organizerURI),
588
-				htmlspecialchars($organizerName ?: $organizerEmail));
589
-			$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
590
-			if (isset($organizer['PARTSTAT'])) {
591
-				/** @var Parameter $partstat */
592
-				$partstat = $organizer['PARTSTAT'];
593
-				if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
594
-					$organizerHTML .= ' ✔︎';
595
-					$organizerText .= ' ✔︎';
596
-				}
597
-			}
598
-			$template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'),
599
-				$this->getAbsoluteImagePath('caldav/organizer.png'),
600
-				$organizerText, '', self::IMIP_INDENT);
601
-		}
602
-
603
-		$attendees = $vevent->select('ATTENDEE');
604
-		if (count($attendees) === 0) {
605
-			return;
606
-		}
607
-
608
-		$attendeesHTML = [];
609
-		$attendeesText = [];
610
-		foreach ($attendees as $attendee) {
611
-			$attendeeURI = $attendee->getNormalizedValue();
612
-			[$scheme,$attendeeEmail] = explode(':', $attendeeURI, 2); # strip off scheme mailto:
613
-			$attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
614
-			$attendeeHTML = sprintf('<a href="%s">%s</a>',
615
-				htmlspecialchars($attendeeURI),
616
-				htmlspecialchars($attendeeName ?: $attendeeEmail));
617
-			$attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
618
-			if (isset($attendee['PARTSTAT'])
619
-				&& strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) {
620
-				$attendeeHTML .= ' ✔︎';
621
-				$attendeeText .= ' ✔︎';
622
-			}
623
-			array_push($attendeesHTML, $attendeeHTML);
624
-			array_push($attendeesText, $attendeeText);
625
-		}
626
-
627
-		$template->addBodyListItem(implode('<br/>', $attendeesHTML), $l10n->t('Attendees:'),
628
-			$this->getAbsoluteImagePath('caldav/attendees.png'),
629
-			implode("\n", $attendeesText), '', self::IMIP_INDENT);
630
-	}
631
-
632
-	/**
633
-	 * @param IEMailTemplate $template
634
-	 * @param IL10N $l10n
635
-	 * @param Message $iTipMessage
636
-	 * @param int $lastOccurrence
637
-	 */
638
-	private function addResponseButtons(IEMailTemplate $template, IL10N $l10n,
639
-										Message $iTipMessage, $lastOccurrence) {
640
-		$token = $this->createInvitationToken($iTipMessage, $lastOccurrence);
641
-
642
-		$template->addBodyButtonGroup(
643
-			$l10n->t('Accept'),
644
-			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
645
-				'token' => $token,
646
-			]),
647
-			$l10n->t('Decline'),
648
-			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
649
-				'token' => $token,
650
-			])
651
-		);
652
-
653
-		$moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
654
-			'token' => $token,
655
-		]);
656
-		$html = vsprintf('<small><a href="%s">%s</a></small>', [
657
-			$moreOptionsURL, $l10n->t('More options …')
658
-		]);
659
-		$text = $l10n->t('More options at %s', [$moreOptionsURL]);
660
-
661
-		$template->addBodyText($html, $text);
662
-	}
663
-
664
-	/**
665
-	 * @param string $path
666
-	 * @return string
667
-	 */
668
-	private function getAbsoluteImagePath($path) {
669
-		return $this->urlGenerator->getAbsoluteURL(
670
-			$this->urlGenerator->imagePath('core', $path)
671
-		);
672
-	}
673
-
674
-	/**
675
-	 * @param Message $iTipMessage
676
-	 * @param int $lastOccurrence
677
-	 * @return string
678
-	 */
679
-	private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
680
-		$token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
681
-
682
-		/** @var VEvent $vevent */
683
-		$vevent = $iTipMessage->message->VEVENT;
684
-		$attendee = $iTipMessage->recipient;
685
-		$organizer = $iTipMessage->sender;
686
-		$sequence = $iTipMessage->sequence;
687
-		$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
688
-			$vevent->{'RECURRENCE-ID'}->serialize() : null;
689
-		$uid = $vevent->{'UID'};
690
-
691
-		$query = $this->db->getQueryBuilder();
692
-		$query->insert('calendar_invitations')
693
-			->values([
694
-				'token' => $query->createNamedParameter($token),
695
-				'attendee' => $query->createNamedParameter($attendee),
696
-				'organizer' => $query->createNamedParameter($organizer),
697
-				'sequence' => $query->createNamedParameter($sequence),
698
-				'recurrenceid' => $query->createNamedParameter($recurrenceId),
699
-				'expiration' => $query->createNamedParameter($lastOccurrence),
700
-				'uid' => $query->createNamedParameter($uid)
701
-			])
702
-			->execute();
703
-
704
-		return $token;
705
-	}
257
+            $recipientDomain = substr(strrchr($recipient, "@"), 1);
258
+            $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
259
+
260
+            if (strcmp('yes', $invitationLinkRecipients[0]) === 0
261
+                 || in_array(strtolower($recipient), $invitationLinkRecipients)
262
+                 || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
263
+                $this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence);
264
+            }
265
+        }
266
+
267
+        $template->addFooter();
268
+
269
+        $message->useTemplate($template);
270
+
271
+        $attachment = $this->mailer->createAttachment(
272
+            $iTipMessage->message->serialize(),
273
+            'event.ics',// TODO(leon): Make file name unique, e.g. add event id
274
+            'text/calendar; method=' . $iTipMessage->method
275
+        );
276
+        $message->attach($attachment);
277
+
278
+        try {
279
+            $failed = $this->mailer->send($message);
280
+            $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
281
+            if ($failed) {
282
+                $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
283
+                $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
284
+            }
285
+        } catch (\Exception $ex) {
286
+            $this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]);
287
+            $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
288
+        }
289
+    }
290
+
291
+    /**
292
+     * check if event took place in the past already
293
+     * @param VCalendar $vObject
294
+     * @return int
295
+     */
296
+    private function getLastOccurrence(VCalendar $vObject) {
297
+        /** @var VEvent $component */
298
+        $component = $vObject->VEVENT;
299
+
300
+        $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
301
+        // Finding the last occurrence is a bit harder
302
+        if (!isset($component->RRULE)) {
303
+            if (isset($component->DTEND)) {
304
+                $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
305
+            } elseif (isset($component->DURATION)) {
306
+                /** @var \DateTime $endDate */
307
+                $endDate = clone $component->DTSTART->getDateTime();
308
+                // $component->DTEND->getDateTime() returns DateTimeImmutable
309
+                $endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
310
+                $lastOccurrence = $endDate->getTimestamp();
311
+            } elseif (!$component->DTSTART->hasTime()) {
312
+                /** @var \DateTime $endDate */
313
+                $endDate = clone $component->DTSTART->getDateTime();
314
+                // $component->DTSTART->getDateTime() returns DateTimeImmutable
315
+                $endDate = $endDate->modify('+1 day');
316
+                $lastOccurrence = $endDate->getTimestamp();
317
+            } else {
318
+                $lastOccurrence = $firstOccurrence;
319
+            }
320
+        } else {
321
+            $it = new EventIterator($vObject, (string)$component->UID);
322
+            $maxDate = new \DateTime(self::MAX_DATE);
323
+            if ($it->isInfinite()) {
324
+                $lastOccurrence = $maxDate->getTimestamp();
325
+            } else {
326
+                $end = $it->getDtEnd();
327
+                while ($it->valid() && $end < $maxDate) {
328
+                    $end = $it->getDtEnd();
329
+                    $it->next();
330
+                }
331
+                $lastOccurrence = $end->getTimestamp();
332
+            }
333
+        }
334
+
335
+        return $lastOccurrence;
336
+    }
337
+
338
+    /**
339
+     * @param Message $iTipMessage
340
+     * @return null|Property
341
+     */
342
+    private function getCurrentAttendee(Message $iTipMessage) {
343
+        /** @var VEvent $vevent */
344
+        $vevent = $iTipMessage->message->VEVENT;
345
+        $attendees = $vevent->select('ATTENDEE');
346
+        foreach ($attendees as $attendee) {
347
+            /** @var Property $attendee */
348
+            if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
349
+                return $attendee;
350
+            }
351
+        }
352
+        return null;
353
+    }
354
+
355
+    /**
356
+     * @param string $default
357
+     * @param Property|null $attendee
358
+     * @return string
359
+     */
360
+    private function getAttendeeLangOrDefault($default, Property $attendee = null) {
361
+        if ($attendee !== null) {
362
+            $lang = $attendee->offsetGet('LANGUAGE');
363
+            if ($lang instanceof Parameter) {
364
+                return $lang->getValue();
365
+            }
366
+        }
367
+        return $default;
368
+    }
369
+
370
+    /**
371
+     * @param Property|null $attendee
372
+     * @return bool
373
+     */
374
+    private function getAttendeeRsvpOrReqForParticipant(Property $attendee = null) {
375
+        if ($attendee !== null) {
376
+            $rsvp = $attendee->offsetGet('RSVP');
377
+            if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
378
+                return true;
379
+            }
380
+            $role = $attendee->offsetGet('ROLE');
381
+            // @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16
382
+            // Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set
383
+            if ($role === null
384
+                || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0))
385
+                || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0))
386
+            ) {
387
+                return true;
388
+            }
389
+        }
390
+        // RFC 5545 3.2.17: default RSVP is false
391
+        return false;
392
+    }
393
+
394
+    /**
395
+     * @param IL10N $l10n
396
+     * @param VEvent $vevent
397
+     */
398
+    private function generateWhenString(IL10N $l10n, VEvent $vevent) {
399
+        $dtstart = $vevent->DTSTART;
400
+        if (isset($vevent->DTEND)) {
401
+            $dtend = $vevent->DTEND;
402
+        } elseif (isset($vevent->DURATION)) {
403
+            $isFloating = $vevent->DTSTART->isFloating();
404
+            $dtend = clone $vevent->DTSTART;
405
+            $endDateTime = $dtend->getDateTime();
406
+            $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
407
+            $dtend->setDateTime($endDateTime, $isFloating);
408
+        } elseif (!$vevent->DTSTART->hasTime()) {
409
+            $isFloating = $vevent->DTSTART->isFloating();
410
+            $dtend = clone $vevent->DTSTART;
411
+            $endDateTime = $dtend->getDateTime();
412
+            $endDateTime = $endDateTime->modify('+1 day');
413
+            $dtend->setDateTime($endDateTime, $isFloating);
414
+        } else {
415
+            $dtend = clone $vevent->DTSTART;
416
+        }
417
+
418
+        $isAllDay = $dtstart instanceof Property\ICalendar\Date;
419
+
420
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
421
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
422
+        /** @var \DateTimeImmutable $dtstartDt */
423
+        $dtstartDt = $dtstart->getDateTime();
424
+        /** @var \DateTimeImmutable $dtendDt */
425
+        $dtendDt = $dtend->getDateTime();
426
+
427
+        $diff = $dtstartDt->diff($dtendDt);
428
+
429
+        $dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
430
+        $dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
431
+
432
+        if ($isAllDay) {
433
+            // One day event
434
+            if ($diff->days === 1) {
435
+                return $l10n->l('date', $dtstartDt, ['width' => 'medium']);
436
+            }
437
+
438
+            // DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
439
+            // the email should show 2020-01-01 to 2020-01-04.
440
+            $dtendDt->modify('-1 day');
441
+
442
+            //event that spans over multiple days
443
+            $localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
444
+            $localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
445
+
446
+            return $localeStart . ' - ' . $localeEnd;
447
+        }
448
+
449
+        /** @var Property\ICalendar\DateTime $dtstart */
450
+        /** @var Property\ICalendar\DateTime $dtend */
451
+        $isFloating = $dtstart->isFloating();
452
+        $startTimezone = $endTimezone = null;
453
+        if (!$isFloating) {
454
+            $prop = $dtstart->offsetGet('TZID');
455
+            if ($prop instanceof Parameter) {
456
+                $startTimezone = $prop->getValue();
457
+            }
458
+
459
+            $prop = $dtend->offsetGet('TZID');
460
+            if ($prop instanceof Parameter) {
461
+                $endTimezone = $prop->getValue();
462
+            }
463
+        }
464
+
465
+        $localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
466
+            $l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
467
+
468
+        // always show full date with timezone if timezones are different
469
+        if ($startTimezone !== $endTimezone) {
470
+            $localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
471
+
472
+            return $localeStart . ' (' . $startTimezone . ') - ' .
473
+                $localeEnd . ' (' . $endTimezone . ')';
474
+        }
475
+
476
+        // show only end time if date is the same
477
+        if ($this->isDayEqual($dtstartDt, $dtendDt)) {
478
+            $localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
479
+        } else {
480
+            $localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
481
+                $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
482
+        }
483
+
484
+        return  $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
485
+    }
486
+
487
+    /**
488
+     * @param \DateTime $dtStart
489
+     * @param \DateTime $dtEnd
490
+     * @return bool
491
+     */
492
+    private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) {
493
+        return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
494
+    }
495
+
496
+    /**
497
+     * @param IEMailTemplate $template
498
+     * @param IL10N $l10n
499
+     * @param string $method
500
+     * @param string $summary
501
+     */
502
+    private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
503
+                                            $method, $summary) {
504
+        if ($method === self::METHOD_CANCEL) {
505
+            // TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}"
506
+            $template->setSubject($l10n->t('Cancelled: %1$s', [$summary]));
507
+            $template->addHeading($l10n->t('Invitation canceled'));
508
+        } elseif ($method === self::METHOD_REPLY) {
509
+            // TRANSLATORS Subject for email, when an invitation is updated. Ex: "Re: {{Event Name}}"
510
+            $template->setSubject($l10n->t('Re: %1$s', [$summary]));
511
+            $template->addHeading($l10n->t('Invitation updated'));
512
+        } else {
513
+            // TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}"
514
+            $template->setSubject($l10n->t('Invitation: %1$s', [$summary]));
515
+            $template->addHeading($l10n->t('Invitation'));
516
+        }
517
+    }
518
+
519
+    /**
520
+     * @param IEMailTemplate $template
521
+     * @param IL10N $l10n
522
+     * @param VEVENT $vevent
523
+     */
524
+    private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) {
525
+        if ($vevent->SUMMARY) {
526
+            $template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'),
527
+                $this->getAbsoluteImagePath('caldav/title.png'), '', '', self::IMIP_INDENT);
528
+        }
529
+        $meetingWhen = $this->generateWhenString($l10n, $vevent);
530
+        if ($meetingWhen) {
531
+            $template->addBodyListItem($meetingWhen, $l10n->t('Time:'),
532
+                $this->getAbsoluteImagePath('caldav/time.png'), '', '', self::IMIP_INDENT);
533
+        }
534
+        if ($vevent->LOCATION) {
535
+            $template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'),
536
+                $this->getAbsoluteImagePath('caldav/location.png'), '', '', self::IMIP_INDENT);
537
+        }
538
+        if ($vevent->URL) {
539
+            $url = $vevent->URL->getValue();
540
+            $template->addBodyListItem(sprintf('<a href="%s">%s</a>',
541
+                htmlspecialchars($url),
542
+                htmlspecialchars($url)),
543
+                $l10n->t('Link:'),
544
+                $this->getAbsoluteImagePath('caldav/link.png'),
545
+                $url, '', self::IMIP_INDENT);
546
+        }
547
+
548
+        $this->addAttendees($template, $l10n, $vevent);
549
+
550
+        /* Put description last, like an email body, since it can be arbitrarily long */
551
+        if ($vevent->DESCRIPTION) {
552
+            $template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'),
553
+                $this->getAbsoluteImagePath('caldav/description.png'), '', '', self::IMIP_INDENT);
554
+        }
555
+    }
556
+
557
+    /**
558
+     * addAttendees: add organizer and attendee names/emails to iMip mail.
559
+     *
560
+     * Enable with DAV setting: invitation_list_attendees (default: no)
561
+     *
562
+     * The default is 'no', which matches old behavior, and is privacy preserving.
563
+     *
564
+     * To enable including attendees in invitation emails:
565
+     *   % php occ config:app:set dav invitation_list_attendees --value yes
566
+     *
567
+     * @param IEMailTemplate $template
568
+     * @param IL10N $l10n
569
+     * @param Message $iTipMessage
570
+     * @param int $lastOccurrence
571
+     * @author brad2014 on github.com
572
+     */
573
+
574
+    private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) {
575
+        if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
576
+            return;
577
+        }
578
+
579
+        if (isset($vevent->ORGANIZER)) {
580
+            /** @var Property\ICalendar\CalAddress $organizer */
581
+            $organizer = $vevent->ORGANIZER;
582
+            $organizerURI = $organizer->getNormalizedValue();
583
+            [$scheme,$organizerEmail] = explode(':', $organizerURI, 2); # strip off scheme mailto:
584
+            /** @var string|null $organizerName */
585
+            $organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
586
+            $organizerHTML = sprintf('<a href="%s">%s</a>',
587
+                htmlspecialchars($organizerURI),
588
+                htmlspecialchars($organizerName ?: $organizerEmail));
589
+            $organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
590
+            if (isset($organizer['PARTSTAT'])) {
591
+                /** @var Parameter $partstat */
592
+                $partstat = $organizer['PARTSTAT'];
593
+                if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
594
+                    $organizerHTML .= ' ✔︎';
595
+                    $organizerText .= ' ✔︎';
596
+                }
597
+            }
598
+            $template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'),
599
+                $this->getAbsoluteImagePath('caldav/organizer.png'),
600
+                $organizerText, '', self::IMIP_INDENT);
601
+        }
602
+
603
+        $attendees = $vevent->select('ATTENDEE');
604
+        if (count($attendees) === 0) {
605
+            return;
606
+        }
607
+
608
+        $attendeesHTML = [];
609
+        $attendeesText = [];
610
+        foreach ($attendees as $attendee) {
611
+            $attendeeURI = $attendee->getNormalizedValue();
612
+            [$scheme,$attendeeEmail] = explode(':', $attendeeURI, 2); # strip off scheme mailto:
613
+            $attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
614
+            $attendeeHTML = sprintf('<a href="%s">%s</a>',
615
+                htmlspecialchars($attendeeURI),
616
+                htmlspecialchars($attendeeName ?: $attendeeEmail));
617
+            $attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
618
+            if (isset($attendee['PARTSTAT'])
619
+                && strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) {
620
+                $attendeeHTML .= ' ✔︎';
621
+                $attendeeText .= ' ✔︎';
622
+            }
623
+            array_push($attendeesHTML, $attendeeHTML);
624
+            array_push($attendeesText, $attendeeText);
625
+        }
626
+
627
+        $template->addBodyListItem(implode('<br/>', $attendeesHTML), $l10n->t('Attendees:'),
628
+            $this->getAbsoluteImagePath('caldav/attendees.png'),
629
+            implode("\n", $attendeesText), '', self::IMIP_INDENT);
630
+    }
631
+
632
+    /**
633
+     * @param IEMailTemplate $template
634
+     * @param IL10N $l10n
635
+     * @param Message $iTipMessage
636
+     * @param int $lastOccurrence
637
+     */
638
+    private function addResponseButtons(IEMailTemplate $template, IL10N $l10n,
639
+                                        Message $iTipMessage, $lastOccurrence) {
640
+        $token = $this->createInvitationToken($iTipMessage, $lastOccurrence);
641
+
642
+        $template->addBodyButtonGroup(
643
+            $l10n->t('Accept'),
644
+            $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
645
+                'token' => $token,
646
+            ]),
647
+            $l10n->t('Decline'),
648
+            $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
649
+                'token' => $token,
650
+            ])
651
+        );
652
+
653
+        $moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
654
+            'token' => $token,
655
+        ]);
656
+        $html = vsprintf('<small><a href="%s">%s</a></small>', [
657
+            $moreOptionsURL, $l10n->t('More options …')
658
+        ]);
659
+        $text = $l10n->t('More options at %s', [$moreOptionsURL]);
660
+
661
+        $template->addBodyText($html, $text);
662
+    }
663
+
664
+    /**
665
+     * @param string $path
666
+     * @return string
667
+     */
668
+    private function getAbsoluteImagePath($path) {
669
+        return $this->urlGenerator->getAbsoluteURL(
670
+            $this->urlGenerator->imagePath('core', $path)
671
+        );
672
+    }
673
+
674
+    /**
675
+     * @param Message $iTipMessage
676
+     * @param int $lastOccurrence
677
+     * @return string
678
+     */
679
+    private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
680
+        $token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
681
+
682
+        /** @var VEvent $vevent */
683
+        $vevent = $iTipMessage->message->VEVENT;
684
+        $attendee = $iTipMessage->recipient;
685
+        $organizer = $iTipMessage->sender;
686
+        $sequence = $iTipMessage->sequence;
687
+        $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
688
+            $vevent->{'RECURRENCE-ID'}->serialize() : null;
689
+        $uid = $vevent->{'UID'};
690
+
691
+        $query = $this->db->getQueryBuilder();
692
+        $query->insert('calendar_invitations')
693
+            ->values([
694
+                'token' => $query->createNamedParameter($token),
695
+                'attendee' => $query->createNamedParameter($attendee),
696
+                'organizer' => $query->createNamedParameter($organizer),
697
+                'sequence' => $query->createNamedParameter($sequence),
698
+                'recurrenceid' => $query->createNamedParameter($recurrenceId),
699
+                'expiration' => $query->createNamedParameter($lastOccurrence),
700
+                'uid' => $query->createNamedParameter($uid)
701
+            ])
702
+            ->execute();
703
+
704
+        return $token;
705
+    }
706 706
 }
Please login to merge, or discard this patch.
Spacing   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -210,11 +210,11 @@  discard block
 block discarded – undo
210 210
 		}
211 211
 
212 212
 		$data = [
213
-			'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
214
-			'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
215
-			'meeting_title' => (string)$meetingTitle ?: $defaultVal,
216
-			'meeting_description' => (string)$meetingDescription ?: $defaultVal,
217
-			'meeting_url' => (string)$meetingUrl ?: $defaultVal,
213
+			'attendee_name' => (string) $meetingAttendeeName ?: $defaultVal,
214
+			'invitee_name' => (string) $meetingInviteeName ?: $defaultVal,
215
+			'meeting_title' => (string) $meetingTitle ?: $defaultVal,
216
+			'meeting_description' => (string) $meetingDescription ?: $defaultVal,
217
+			'meeting_url' => (string) $meetingUrl ?: $defaultVal,
218 218
 		];
219 219
 
220 220
 		$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
@@ -225,7 +225,7 @@  discard block
 block discarded – undo
225 225
 			->setTo([$recipient => $recipientName])
226 226
 			->setReplyTo([$sender => $senderName]);
227 227
 
228
-		$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
228
+		$template = $this->mailer->createEMailTemplate('dav.calendarInvite.'.$method, $data);
229 229
 		$template->addHeader();
230 230
 
231 231
 		$summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
@@ -270,8 +270,8 @@  discard block
 block discarded – undo
270 270
 
271 271
 		$attachment = $this->mailer->createAttachment(
272 272
 			$iTipMessage->message->serialize(),
273
-			'event.ics',// TODO(leon): Make file name unique, e.g. add event id
274
-			'text/calendar; method=' . $iTipMessage->method
273
+			'event.ics', // TODO(leon): Make file name unique, e.g. add event id
274
+			'text/calendar; method='.$iTipMessage->method
275 275
 		);
276 276
 		$message->attach($attachment);
277 277
 
@@ -318,7 +318,7 @@  discard block
 block discarded – undo
318 318
 				$lastOccurrence = $firstOccurrence;
319 319
 			}
320 320
 		} else {
321
-			$it = new EventIterator($vObject, (string)$component->UID);
321
+			$it = new EventIterator($vObject, (string) $component->UID);
322 322
 			$maxDate = new \DateTime(self::MAX_DATE);
323 323
 			if ($it->isInfinite()) {
324 324
 				$lastOccurrence = $maxDate->getTimestamp();
@@ -443,7 +443,7 @@  discard block
 block discarded – undo
443 443
 			$localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
444 444
 			$localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
445 445
 
446
-			return $localeStart . ' - ' . $localeEnd;
446
+			return $localeStart.' - '.$localeEnd;
447 447
 		}
448 448
 
449 449
 		/** @var Property\ICalendar\DateTime $dtstart */
@@ -462,26 +462,26 @@  discard block
 block discarded – undo
462 462
 			}
463 463
 		}
464 464
 
465
-		$localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
465
+		$localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']).', '.
466 466
 			$l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
467 467
 
468 468
 		// always show full date with timezone if timezones are different
469 469
 		if ($startTimezone !== $endTimezone) {
470 470
 			$localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
471 471
 
472
-			return $localeStart . ' (' . $startTimezone . ') - ' .
473
-				$localeEnd . ' (' . $endTimezone . ')';
472
+			return $localeStart.' ('.$startTimezone.') - '.
473
+				$localeEnd.' ('.$endTimezone.')';
474 474
 		}
475 475
 
476 476
 		// show only end time if date is the same
477 477
 		if ($this->isDayEqual($dtstartDt, $dtendDt)) {
478 478
 			$localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
479 479
 		} else {
480
-			$localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
480
+			$localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']).', '.
481 481
 				$l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
482 482
 		}
483 483
 
484
-		return  $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
484
+		return  $localeStart.' - '.$localeEnd.' ('.$startTimezone.')';
485 485
 	}
486 486
 
487 487
 	/**
@@ -580,7 +580,7 @@  discard block
 block discarded – undo
580 580
 			/** @var Property\ICalendar\CalAddress $organizer */
581 581
 			$organizer = $vevent->ORGANIZER;
582 582
 			$organizerURI = $organizer->getNormalizedValue();
583
-			[$scheme,$organizerEmail] = explode(':', $organizerURI, 2); # strip off scheme mailto:
583
+			[$scheme, $organizerEmail] = explode(':', $organizerURI, 2); # strip off scheme mailto:
584 584
 			/** @var string|null $organizerName */
585 585
 			$organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
586 586
 			$organizerHTML = sprintf('<a href="%s">%s</a>',
@@ -609,7 +609,7 @@  discard block
 block discarded – undo
609 609
 		$attendeesText = [];
610 610
 		foreach ($attendees as $attendee) {
611 611
 			$attendeeURI = $attendee->getNormalizedValue();
612
-			[$scheme,$attendeeEmail] = explode(':', $attendeeURI, 2); # strip off scheme mailto:
612
+			[$scheme, $attendeeEmail] = explode(':', $attendeeURI, 2); # strip off scheme mailto:
613 613
 			$attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
614 614
 			$attendeeHTML = sprintf('<a href="%s">%s</a>',
615 615
 				htmlspecialchars($attendeeURI),
Please login to merge, or discard this patch.
apps/encryption/lib/Crypto/Crypt.php 1 patch
Indentation   +697 added lines, -697 removed lines patch added patch discarded remove patch
@@ -56,701 +56,701 @@
 block discarded – undo
56 56
  * @package OCA\Encryption\Crypto
57 57
  */
58 58
 class Crypt {
59
-	public const SUPPORTED_CIPHERS_AND_KEY_SIZE = [
60
-		'AES-256-CTR' => 32,
61
-		'AES-128-CTR' => 16,
62
-		'AES-256-CFB' => 32,
63
-		'AES-128-CFB' => 16,
64
-	];
65
-	// one out of SUPPORTED_CIPHERS_AND_KEY_SIZE
66
-	public const DEFAULT_CIPHER = 'AES-256-CTR';
67
-	// default cipher from old Nextcloud versions
68
-	public const LEGACY_CIPHER = 'AES-128-CFB';
69
-
70
-	public const SUPPORTED_KEY_FORMATS = ['hash', 'password'];
71
-	// one out of SUPPORTED_KEY_FORMATS
72
-	public const DEFAULT_KEY_FORMAT = 'hash';
73
-	// default key format, old Nextcloud version encrypted the private key directly
74
-	// with the user password
75
-	public const LEGACY_KEY_FORMAT = 'password';
76
-
77
-	public const HEADER_START = 'HBEGIN';
78
-	public const HEADER_END = 'HEND';
79
-
80
-	// default encoding format, old Nextcloud versions used base64
81
-	public const BINARY_ENCODING_FORMAT = 'binary';
82
-
83
-	/** @var ILogger */
84
-	private $logger;
85
-
86
-	/** @var string */
87
-	private $user;
88
-
89
-	/** @var IConfig */
90
-	private $config;
91
-
92
-	/** @var IL10N */
93
-	private $l;
94
-
95
-	/** @var string|null */
96
-	private $currentCipher;
97
-
98
-	/** @var bool */
99
-	private $supportLegacy;
100
-
101
-	/**
102
-	 * Use the legacy base64 encoding instead of the more space-efficient binary encoding.
103
-	 */
104
-	private bool $useLegacyBase64Encoding;
105
-
106
-	/**
107
-	 * @param ILogger $logger
108
-	 * @param IUserSession $userSession
109
-	 * @param IConfig $config
110
-	 * @param IL10N $l
111
-	 */
112
-	public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
113
-		$this->logger = $logger;
114
-		$this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
115
-		$this->config = $config;
116
-		$this->l = $l;
117
-		$this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
118
-		$this->useLegacyBase64Encoding = $this->config->getSystemValueBool('encryption.use_legacy_base64_encoding', false);
119
-	}
120
-
121
-	/**
122
-	 * create new private/public key-pair for user
123
-	 *
124
-	 * @return array|bool
125
-	 */
126
-	public function createKeyPair() {
127
-		$log = $this->logger;
128
-		$res = $this->getOpenSSLPKey();
129
-
130
-		if (!$res) {
131
-			$log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
132
-				['app' => 'encryption']);
133
-
134
-			if (openssl_error_string()) {
135
-				$log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
136
-					['app' => 'encryption']);
137
-			}
138
-		} elseif (openssl_pkey_export($res,
139
-			$privateKey,
140
-			null,
141
-			$this->getOpenSSLConfig())) {
142
-			$keyDetails = openssl_pkey_get_details($res);
143
-			$publicKey = $keyDetails['key'];
144
-
145
-			return [
146
-				'publicKey' => $publicKey,
147
-				'privateKey' => $privateKey
148
-			];
149
-		}
150
-		$log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
151
-			['app' => 'encryption']);
152
-		if (openssl_error_string()) {
153
-			$log->error('Encryption Library:' . openssl_error_string(),
154
-				['app' => 'encryption']);
155
-		}
156
-
157
-		return false;
158
-	}
159
-
160
-	/**
161
-	 * Generates a new private key
162
-	 *
163
-	 * @return \OpenSSLAsymmetricKey|false
164
-	 */
165
-	public function getOpenSSLPKey() {
166
-		$config = $this->getOpenSSLConfig();
167
-		return openssl_pkey_new($config);
168
-	}
169
-
170
-	/**
171
-	 * get openSSL Config
172
-	 *
173
-	 * @return array
174
-	 */
175
-	private function getOpenSSLConfig() {
176
-		$config = ['private_key_bits' => 4096];
177
-		$config = array_merge(
178
-			$config,
179
-			$this->config->getSystemValue('openssl', [])
180
-		);
181
-		return $config;
182
-	}
183
-
184
-	/**
185
-	 * @param string $plainContent
186
-	 * @param string $passPhrase
187
-	 * @param int $version
188
-	 * @param int $position
189
-	 * @return false|string
190
-	 * @throws EncryptionFailedException
191
-	 */
192
-	public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
193
-		if (!$plainContent) {
194
-			$this->logger->error('Encryption Library, symmetrical encryption failed no content given',
195
-				['app' => 'encryption']);
196
-			return false;
197
-		}
198
-
199
-		$iv = $this->generateIv();
200
-
201
-		$encryptedContent = $this->encrypt($plainContent,
202
-			$iv,
203
-			$passPhrase,
204
-			$this->getCipher());
205
-
206
-		// Create a signature based on the key as well as the current version
207
-		$sig = $this->createSignature($encryptedContent, $passPhrase.'_'.$version.'_'.$position);
208
-
209
-		// combine content to encrypt the IV identifier and actual IV
210
-		$catFile = $this->concatIV($encryptedContent, $iv);
211
-		$catFile = $this->concatSig($catFile, $sig);
212
-		return $this->addPadding($catFile);
213
-	}
214
-
215
-	/**
216
-	 * generate header for encrypted file
217
-	 *
218
-	 * @param string $keyFormat see SUPPORTED_KEY_FORMATS
219
-	 * @return string
220
-	 * @throws \InvalidArgumentException
221
-	 */
222
-	public function generateHeader($keyFormat = self::DEFAULT_KEY_FORMAT) {
223
-		if (in_array($keyFormat, self::SUPPORTED_KEY_FORMATS, true) === false) {
224
-			throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
225
-		}
226
-
227
-		$header = self::HEADER_START
228
-			. ':cipher:' . $this->getCipher()
229
-			. ':keyFormat:' . $keyFormat;
230
-
231
-		if ($this->useLegacyBase64Encoding !== true) {
232
-			$header .= ':encoding:' . self::BINARY_ENCODING_FORMAT;
233
-		}
234
-
235
-		$header .= ':' . self::HEADER_END;
236
-
237
-		return $header;
238
-	}
239
-
240
-	/**
241
-	 * @param string $plainContent
242
-	 * @param string $iv
243
-	 * @param string $passPhrase
244
-	 * @param string $cipher
245
-	 * @return string
246
-	 * @throws EncryptionFailedException
247
-	 */
248
-	private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
249
-		$options = $this->useLegacyBase64Encoding ? 0 : OPENSSL_RAW_DATA;
250
-		$encryptedContent = openssl_encrypt($plainContent,
251
-			$cipher,
252
-			$passPhrase,
253
-			$options,
254
-			$iv);
255
-
256
-		if (!$encryptedContent) {
257
-			$error = 'Encryption (symmetric) of content failed';
258
-			$this->logger->error($error . openssl_error_string(),
259
-				['app' => 'encryption']);
260
-			throw new EncryptionFailedException($error);
261
-		}
262
-
263
-		return $encryptedContent;
264
-	}
265
-
266
-	/**
267
-	 * return cipher either from config.php or the default cipher defined in
268
-	 * this class
269
-	 *
270
-	 * @return string
271
-	 */
272
-	private function getCachedCipher() {
273
-		if (isset($this->currentCipher)) {
274
-			return $this->currentCipher;
275
-		}
276
-
277
-		// Get cipher either from config.php or the default cipher defined in this class
278
-		$cipher = $this->config->getSystemValueString('cipher', self::DEFAULT_CIPHER);
279
-		if (!isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
280
-			$this->logger->warning(
281
-				sprintf(
282
-					'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
283
-					$cipher,
284
-					self::DEFAULT_CIPHER
285
-				),
286
-				['app' => 'encryption']
287
-			);
288
-			$cipher = self::DEFAULT_CIPHER;
289
-		}
290
-
291
-		// Remember current cipher to avoid frequent lookups
292
-		$this->currentCipher = $cipher;
293
-		return $this->currentCipher;
294
-	}
295
-
296
-	/**
297
-	 * return current encryption cipher
298
-	 *
299
-	 * @return string
300
-	 */
301
-	public function getCipher() {
302
-		return $this->getCachedCipher();
303
-	}
304
-
305
-	/**
306
-	 * get key size depending on the cipher
307
-	 *
308
-	 * @param string $cipher
309
-	 * @return int
310
-	 * @throws \InvalidArgumentException
311
-	 */
312
-	protected function getKeySize($cipher) {
313
-		if (isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
314
-			return self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher];
315
-		}
316
-
317
-		throw new \InvalidArgumentException(
318
-			sprintf(
319
-					'Unsupported cipher (%s) defined.',
320
-					$cipher
321
-			)
322
-		);
323
-	}
324
-
325
-	/**
326
-	 * get legacy cipher
327
-	 *
328
-	 * @return string
329
-	 */
330
-	public function getLegacyCipher() {
331
-		if (!$this->supportLegacy) {
332
-			throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
333
-		}
334
-
335
-		return self::LEGACY_CIPHER;
336
-	}
337
-
338
-	/**
339
-	 * @param string $encryptedContent
340
-	 * @param string $iv
341
-	 * @return string
342
-	 */
343
-	private function concatIV($encryptedContent, $iv) {
344
-		return $encryptedContent . '00iv00' . $iv;
345
-	}
346
-
347
-	/**
348
-	 * @param string $encryptedContent
349
-	 * @param string $signature
350
-	 * @return string
351
-	 */
352
-	private function concatSig($encryptedContent, $signature) {
353
-		return $encryptedContent . '00sig00' . $signature;
354
-	}
355
-
356
-	/**
357
-	 * Note: This is _NOT_ a padding used for encryption purposes. It is solely
358
-	 * used to achieve the PHP stream size. It has _NOTHING_ to do with the
359
-	 * encrypted content and is not used in any crypto primitive.
360
-	 *
361
-	 * @param string $data
362
-	 * @return string
363
-	 */
364
-	private function addPadding($data) {
365
-		return $data . 'xxx';
366
-	}
367
-
368
-	/**
369
-	 * generate password hash used to encrypt the users private key
370
-	 *
371
-	 * @param string $password
372
-	 * @param string $cipher
373
-	 * @param string $uid only used for user keys
374
-	 * @return string
375
-	 */
376
-	protected function generatePasswordHash($password, $cipher, $uid = '') {
377
-		$instanceId = $this->config->getSystemValue('instanceid');
378
-		$instanceSecret = $this->config->getSystemValue('secret');
379
-		$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
380
-		$keySize = $this->getKeySize($cipher);
381
-
382
-		$hash = hash_pbkdf2(
383
-			'sha256',
384
-			$password,
385
-			$salt,
386
-			100000,
387
-			$keySize,
388
-			true
389
-		);
390
-
391
-		return $hash;
392
-	}
393
-
394
-	/**
395
-	 * encrypt private key
396
-	 *
397
-	 * @param string $privateKey
398
-	 * @param string $password
399
-	 * @param string $uid for regular users, empty for system keys
400
-	 * @return false|string
401
-	 */
402
-	public function encryptPrivateKey($privateKey, $password, $uid = '') {
403
-		$cipher = $this->getCipher();
404
-		$hash = $this->generatePasswordHash($password, $cipher, $uid);
405
-		$encryptedKey = $this->symmetricEncryptFileContent(
406
-			$privateKey,
407
-			$hash,
408
-			0,
409
-			0
410
-		);
411
-
412
-		return $encryptedKey;
413
-	}
414
-
415
-	/**
416
-	 * @param string $privateKey
417
-	 * @param string $password
418
-	 * @param string $uid for regular users, empty for system keys
419
-	 * @return false|string
420
-	 */
421
-	public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
422
-		$header = $this->parseHeader($privateKey);
423
-
424
-		if (isset($header['cipher'])) {
425
-			$cipher = $header['cipher'];
426
-		} else {
427
-			$cipher = $this->getLegacyCipher();
428
-		}
429
-
430
-		if (isset($header['keyFormat'])) {
431
-			$keyFormat = $header['keyFormat'];
432
-		} else {
433
-			$keyFormat = self::LEGACY_KEY_FORMAT;
434
-		}
435
-
436
-		if ($keyFormat === self::DEFAULT_KEY_FORMAT) {
437
-			$password = $this->generatePasswordHash($password, $cipher, $uid);
438
-		}
439
-
440
-		$binaryEncoding = isset($header['encoding']) && $header['encoding'] === self::BINARY_ENCODING_FORMAT;
441
-
442
-		// If we found a header we need to remove it from the key we want to decrypt
443
-		if (!empty($header)) {
444
-			$privateKey = substr($privateKey,
445
-				strpos($privateKey,
446
-					self::HEADER_END) + strlen(self::HEADER_END));
447
-		}
448
-
449
-		$plainKey = $this->symmetricDecryptFileContent(
450
-			$privateKey,
451
-			$password,
452
-			$cipher,
453
-			0,
454
-			0,
455
-			$binaryEncoding
456
-		);
457
-
458
-		if ($this->isValidPrivateKey($plainKey) === false) {
459
-			return false;
460
-		}
461
-
462
-		return $plainKey;
463
-	}
464
-
465
-	/**
466
-	 * check if it is a valid private key
467
-	 *
468
-	 * @param string $plainKey
469
-	 * @return bool
470
-	 */
471
-	protected function isValidPrivateKey($plainKey) {
472
-		$res = openssl_get_privatekey($plainKey);
473
-		// TODO: remove resource check one php7.4 is not longer supported
474
-		if (is_resource($res) || (is_object($res) && get_class($res) === 'OpenSSLAsymmetricKey')) {
475
-			$sslInfo = openssl_pkey_get_details($res);
476
-			if (isset($sslInfo['key'])) {
477
-				return true;
478
-			}
479
-		}
480
-
481
-		return false;
482
-	}
483
-
484
-	/**
485
-	 * @param string $keyFileContents
486
-	 * @param string $passPhrase
487
-	 * @param string $cipher
488
-	 * @param int $version
489
-	 * @param int|string $position
490
-	 * @param boolean $binaryEncoding
491
-	 * @return string
492
-	 * @throws DecryptionFailedException
493
-	 */
494
-	public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0, bool $binaryEncoding = false) {
495
-		if ($keyFileContents == '') {
496
-			return '';
497
-		}
498
-
499
-		$catFile = $this->splitMetaData($keyFileContents, $cipher);
500
-
501
-		if ($catFile['signature'] !== false) {
502
-			try {
503
-				// First try the new format
504
-				$this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
505
-			} catch (GenericEncryptionException $e) {
506
-				// For compatibility with old files check the version without _
507
-				$this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
508
-			}
509
-		}
510
-
511
-		return $this->decrypt($catFile['encrypted'],
512
-			$catFile['iv'],
513
-			$passPhrase,
514
-			$cipher,
515
-			$binaryEncoding);
516
-	}
517
-
518
-	/**
519
-	 * check for valid signature
520
-	 *
521
-	 * @param string $data
522
-	 * @param string $passPhrase
523
-	 * @param string $expectedSignature
524
-	 * @throws GenericEncryptionException
525
-	 */
526
-	private function checkSignature($data, $passPhrase, $expectedSignature) {
527
-		$enforceSignature = !$this->config->getSystemValueBool('encryption_skip_signature_check', false);
528
-
529
-		$signature = $this->createSignature($data, $passPhrase);
530
-		$isCorrectHash = hash_equals($expectedSignature, $signature);
531
-
532
-		if (!$isCorrectHash && $enforceSignature) {
533
-			throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
534
-		} elseif (!$isCorrectHash && !$enforceSignature) {
535
-			$this->logger->info("Signature check skipped", ['app' => 'encryption']);
536
-		}
537
-	}
538
-
539
-	/**
540
-	 * create signature
541
-	 *
542
-	 * @param string $data
543
-	 * @param string $passPhrase
544
-	 * @return string
545
-	 */
546
-	private function createSignature($data, $passPhrase) {
547
-		$passPhrase = hash('sha512', $passPhrase . 'a', true);
548
-		return hash_hmac('sha256', $data, $passPhrase);
549
-	}
550
-
551
-
552
-	/**
553
-	 * remove padding
554
-	 *
555
-	 * @param string $padded
556
-	 * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
557
-	 * @return string|false
558
-	 */
559
-	private function removePadding($padded, $hasSignature = false) {
560
-		if ($hasSignature === false && substr($padded, -2) === 'xx') {
561
-			return substr($padded, 0, -2);
562
-		} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
563
-			return substr($padded, 0, -3);
564
-		}
565
-		return false;
566
-	}
567
-
568
-	/**
569
-	 * split meta data from encrypted file
570
-	 * Note: for now, we assume that the meta data always start with the iv
571
-	 *       followed by the signature, if available
572
-	 *
573
-	 * @param string $catFile
574
-	 * @param string $cipher
575
-	 * @return array
576
-	 */
577
-	private function splitMetaData($catFile, $cipher) {
578
-		if ($this->hasSignature($catFile, $cipher)) {
579
-			$catFile = $this->removePadding($catFile, true);
580
-			$meta = substr($catFile, -93);
581
-			$iv = substr($meta, strlen('00iv00'), 16);
582
-			$sig = substr($meta, 22 + strlen('00sig00'));
583
-			$encrypted = substr($catFile, 0, -93);
584
-		} else {
585
-			$catFile = $this->removePadding($catFile);
586
-			$meta = substr($catFile, -22);
587
-			$iv = substr($meta, -16);
588
-			$sig = false;
589
-			$encrypted = substr($catFile, 0, -22);
590
-		}
591
-
592
-		return [
593
-			'encrypted' => $encrypted,
594
-			'iv' => $iv,
595
-			'signature' => $sig
596
-		];
597
-	}
598
-
599
-	/**
600
-	 * check if encrypted block is signed
601
-	 *
602
-	 * @param string $catFile
603
-	 * @param string $cipher
604
-	 * @return bool
605
-	 * @throws GenericEncryptionException
606
-	 */
607
-	private function hasSignature($catFile, $cipher) {
608
-		$skipSignatureCheck = $this->config->getSystemValueBool('encryption_skip_signature_check', false);
609
-
610
-		$meta = substr($catFile, -93);
611
-		$signaturePosition = strpos($meta, '00sig00');
612
-
613
-		// If we no longer support the legacy format then everything needs a signature
614
-		if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
615
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
616
-		}
617
-
618
-		// Enforce signature for the new 'CTR' ciphers
619
-		if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
620
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
621
-		}
622
-
623
-		return ($signaturePosition !== false);
624
-	}
625
-
626
-
627
-	/**
628
-	 * @param string $encryptedContent
629
-	 * @param string $iv
630
-	 * @param string $passPhrase
631
-	 * @param string $cipher
632
-	 * @param boolean $binaryEncoding
633
-	 * @return string
634
-	 * @throws DecryptionFailedException
635
-	 */
636
-	private function decrypt(string $encryptedContent, string $iv, string $passPhrase = '', string $cipher = self::DEFAULT_CIPHER, bool $binaryEncoding = false): string {
637
-		$options = $binaryEncoding === true ? OPENSSL_RAW_DATA : 0;
638
-		$plainContent = openssl_decrypt($encryptedContent,
639
-			$cipher,
640
-			$passPhrase,
641
-			$options,
642
-			$iv);
643
-
644
-		if ($plainContent) {
645
-			return $plainContent;
646
-		} else {
647
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
648
-		}
649
-	}
650
-
651
-	/**
652
-	 * @param string $data
653
-	 * @return array
654
-	 */
655
-	protected function parseHeader($data) {
656
-		$result = [];
657
-
658
-		if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
659
-			$endAt = strpos($data, self::HEADER_END);
660
-			$header = substr($data, 0, $endAt + strlen(self::HEADER_END));
661
-
662
-			// +1 not to start with an ':' which would result in empty element at the beginning
663
-			$exploded = explode(':',
664
-				substr($header, strlen(self::HEADER_START) + 1));
665
-
666
-			$element = array_shift($exploded);
667
-
668
-			while ($element !== self::HEADER_END) {
669
-				$result[$element] = array_shift($exploded);
670
-				$element = array_shift($exploded);
671
-			}
672
-		}
673
-
674
-		return $result;
675
-	}
676
-
677
-	/**
678
-	 * generate initialization vector
679
-	 *
680
-	 * @return string
681
-	 * @throws GenericEncryptionException
682
-	 */
683
-	private function generateIv() {
684
-		return random_bytes(16);
685
-	}
686
-
687
-	/**
688
-	 * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
689
-	 * as file key
690
-	 *
691
-	 * @return string
692
-	 * @throws \Exception
693
-	 */
694
-	public function generateFileKey() {
695
-		return random_bytes(32);
696
-	}
697
-
698
-	/**
699
-	 * @param $encKeyFile
700
-	 * @param $shareKey
701
-	 * @param $privateKey
702
-	 * @return string
703
-	 * @throws MultiKeyDecryptException
704
-	 */
705
-	public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
706
-		if (!$encKeyFile) {
707
-			throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
708
-		}
709
-
710
-		if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) {
711
-			return $plainContent;
712
-		} else {
713
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
714
-		}
715
-	}
716
-
717
-	/**
718
-	 * @param string $plainContent
719
-	 * @param array $keyFiles
720
-	 * @return array
721
-	 * @throws MultiKeyEncryptException
722
-	 */
723
-	public function multiKeyEncrypt($plainContent, array $keyFiles) {
724
-		// openssl_seal returns false without errors if plaincontent is empty
725
-		// so trigger our own error
726
-		if (empty($plainContent)) {
727
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
728
-		}
729
-
730
-		// Set empty vars to be set by openssl by reference
731
-		$sealed = '';
732
-		$shareKeys = [];
733
-		$mappedShareKeys = [];
734
-
735
-		if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) {
736
-			$i = 0;
737
-
738
-			// Ensure each shareKey is labelled with its corresponding key id
739
-			foreach ($keyFiles as $userId => $publicKey) {
740
-				$mappedShareKeys[$userId] = $shareKeys[$i];
741
-				$i++;
742
-			}
743
-
744
-			return [
745
-				'keys' => $mappedShareKeys,
746
-				'data' => $sealed
747
-			];
748
-		} else {
749
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
750
-		}
751
-	}
752
-
753
-	public function useLegacyBase64Encoding(): bool {
754
-		return $this->useLegacyBase64Encoding;
755
-	}
59
+    public const SUPPORTED_CIPHERS_AND_KEY_SIZE = [
60
+        'AES-256-CTR' => 32,
61
+        'AES-128-CTR' => 16,
62
+        'AES-256-CFB' => 32,
63
+        'AES-128-CFB' => 16,
64
+    ];
65
+    // one out of SUPPORTED_CIPHERS_AND_KEY_SIZE
66
+    public const DEFAULT_CIPHER = 'AES-256-CTR';
67
+    // default cipher from old Nextcloud versions
68
+    public const LEGACY_CIPHER = 'AES-128-CFB';
69
+
70
+    public const SUPPORTED_KEY_FORMATS = ['hash', 'password'];
71
+    // one out of SUPPORTED_KEY_FORMATS
72
+    public const DEFAULT_KEY_FORMAT = 'hash';
73
+    // default key format, old Nextcloud version encrypted the private key directly
74
+    // with the user password
75
+    public const LEGACY_KEY_FORMAT = 'password';
76
+
77
+    public const HEADER_START = 'HBEGIN';
78
+    public const HEADER_END = 'HEND';
79
+
80
+    // default encoding format, old Nextcloud versions used base64
81
+    public const BINARY_ENCODING_FORMAT = 'binary';
82
+
83
+    /** @var ILogger */
84
+    private $logger;
85
+
86
+    /** @var string */
87
+    private $user;
88
+
89
+    /** @var IConfig */
90
+    private $config;
91
+
92
+    /** @var IL10N */
93
+    private $l;
94
+
95
+    /** @var string|null */
96
+    private $currentCipher;
97
+
98
+    /** @var bool */
99
+    private $supportLegacy;
100
+
101
+    /**
102
+     * Use the legacy base64 encoding instead of the more space-efficient binary encoding.
103
+     */
104
+    private bool $useLegacyBase64Encoding;
105
+
106
+    /**
107
+     * @param ILogger $logger
108
+     * @param IUserSession $userSession
109
+     * @param IConfig $config
110
+     * @param IL10N $l
111
+     */
112
+    public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
113
+        $this->logger = $logger;
114
+        $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
115
+        $this->config = $config;
116
+        $this->l = $l;
117
+        $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
118
+        $this->useLegacyBase64Encoding = $this->config->getSystemValueBool('encryption.use_legacy_base64_encoding', false);
119
+    }
120
+
121
+    /**
122
+     * create new private/public key-pair for user
123
+     *
124
+     * @return array|bool
125
+     */
126
+    public function createKeyPair() {
127
+        $log = $this->logger;
128
+        $res = $this->getOpenSSLPKey();
129
+
130
+        if (!$res) {
131
+            $log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
132
+                ['app' => 'encryption']);
133
+
134
+            if (openssl_error_string()) {
135
+                $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
136
+                    ['app' => 'encryption']);
137
+            }
138
+        } elseif (openssl_pkey_export($res,
139
+            $privateKey,
140
+            null,
141
+            $this->getOpenSSLConfig())) {
142
+            $keyDetails = openssl_pkey_get_details($res);
143
+            $publicKey = $keyDetails['key'];
144
+
145
+            return [
146
+                'publicKey' => $publicKey,
147
+                'privateKey' => $privateKey
148
+            ];
149
+        }
150
+        $log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
151
+            ['app' => 'encryption']);
152
+        if (openssl_error_string()) {
153
+            $log->error('Encryption Library:' . openssl_error_string(),
154
+                ['app' => 'encryption']);
155
+        }
156
+
157
+        return false;
158
+    }
159
+
160
+    /**
161
+     * Generates a new private key
162
+     *
163
+     * @return \OpenSSLAsymmetricKey|false
164
+     */
165
+    public function getOpenSSLPKey() {
166
+        $config = $this->getOpenSSLConfig();
167
+        return openssl_pkey_new($config);
168
+    }
169
+
170
+    /**
171
+     * get openSSL Config
172
+     *
173
+     * @return array
174
+     */
175
+    private function getOpenSSLConfig() {
176
+        $config = ['private_key_bits' => 4096];
177
+        $config = array_merge(
178
+            $config,
179
+            $this->config->getSystemValue('openssl', [])
180
+        );
181
+        return $config;
182
+    }
183
+
184
+    /**
185
+     * @param string $plainContent
186
+     * @param string $passPhrase
187
+     * @param int $version
188
+     * @param int $position
189
+     * @return false|string
190
+     * @throws EncryptionFailedException
191
+     */
192
+    public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
193
+        if (!$plainContent) {
194
+            $this->logger->error('Encryption Library, symmetrical encryption failed no content given',
195
+                ['app' => 'encryption']);
196
+            return false;
197
+        }
198
+
199
+        $iv = $this->generateIv();
200
+
201
+        $encryptedContent = $this->encrypt($plainContent,
202
+            $iv,
203
+            $passPhrase,
204
+            $this->getCipher());
205
+
206
+        // Create a signature based on the key as well as the current version
207
+        $sig = $this->createSignature($encryptedContent, $passPhrase.'_'.$version.'_'.$position);
208
+
209
+        // combine content to encrypt the IV identifier and actual IV
210
+        $catFile = $this->concatIV($encryptedContent, $iv);
211
+        $catFile = $this->concatSig($catFile, $sig);
212
+        return $this->addPadding($catFile);
213
+    }
214
+
215
+    /**
216
+     * generate header for encrypted file
217
+     *
218
+     * @param string $keyFormat see SUPPORTED_KEY_FORMATS
219
+     * @return string
220
+     * @throws \InvalidArgumentException
221
+     */
222
+    public function generateHeader($keyFormat = self::DEFAULT_KEY_FORMAT) {
223
+        if (in_array($keyFormat, self::SUPPORTED_KEY_FORMATS, true) === false) {
224
+            throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
225
+        }
226
+
227
+        $header = self::HEADER_START
228
+            . ':cipher:' . $this->getCipher()
229
+            . ':keyFormat:' . $keyFormat;
230
+
231
+        if ($this->useLegacyBase64Encoding !== true) {
232
+            $header .= ':encoding:' . self::BINARY_ENCODING_FORMAT;
233
+        }
234
+
235
+        $header .= ':' . self::HEADER_END;
236
+
237
+        return $header;
238
+    }
239
+
240
+    /**
241
+     * @param string $plainContent
242
+     * @param string $iv
243
+     * @param string $passPhrase
244
+     * @param string $cipher
245
+     * @return string
246
+     * @throws EncryptionFailedException
247
+     */
248
+    private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
249
+        $options = $this->useLegacyBase64Encoding ? 0 : OPENSSL_RAW_DATA;
250
+        $encryptedContent = openssl_encrypt($plainContent,
251
+            $cipher,
252
+            $passPhrase,
253
+            $options,
254
+            $iv);
255
+
256
+        if (!$encryptedContent) {
257
+            $error = 'Encryption (symmetric) of content failed';
258
+            $this->logger->error($error . openssl_error_string(),
259
+                ['app' => 'encryption']);
260
+            throw new EncryptionFailedException($error);
261
+        }
262
+
263
+        return $encryptedContent;
264
+    }
265
+
266
+    /**
267
+     * return cipher either from config.php or the default cipher defined in
268
+     * this class
269
+     *
270
+     * @return string
271
+     */
272
+    private function getCachedCipher() {
273
+        if (isset($this->currentCipher)) {
274
+            return $this->currentCipher;
275
+        }
276
+
277
+        // Get cipher either from config.php or the default cipher defined in this class
278
+        $cipher = $this->config->getSystemValueString('cipher', self::DEFAULT_CIPHER);
279
+        if (!isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
280
+            $this->logger->warning(
281
+                sprintf(
282
+                    'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
283
+                    $cipher,
284
+                    self::DEFAULT_CIPHER
285
+                ),
286
+                ['app' => 'encryption']
287
+            );
288
+            $cipher = self::DEFAULT_CIPHER;
289
+        }
290
+
291
+        // Remember current cipher to avoid frequent lookups
292
+        $this->currentCipher = $cipher;
293
+        return $this->currentCipher;
294
+    }
295
+
296
+    /**
297
+     * return current encryption cipher
298
+     *
299
+     * @return string
300
+     */
301
+    public function getCipher() {
302
+        return $this->getCachedCipher();
303
+    }
304
+
305
+    /**
306
+     * get key size depending on the cipher
307
+     *
308
+     * @param string $cipher
309
+     * @return int
310
+     * @throws \InvalidArgumentException
311
+     */
312
+    protected function getKeySize($cipher) {
313
+        if (isset(self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher])) {
314
+            return self::SUPPORTED_CIPHERS_AND_KEY_SIZE[$cipher];
315
+        }
316
+
317
+        throw new \InvalidArgumentException(
318
+            sprintf(
319
+                    'Unsupported cipher (%s) defined.',
320
+                    $cipher
321
+            )
322
+        );
323
+    }
324
+
325
+    /**
326
+     * get legacy cipher
327
+     *
328
+     * @return string
329
+     */
330
+    public function getLegacyCipher() {
331
+        if (!$this->supportLegacy) {
332
+            throw new ServerNotAvailableException('Legacy cipher is no longer supported!');
333
+        }
334
+
335
+        return self::LEGACY_CIPHER;
336
+    }
337
+
338
+    /**
339
+     * @param string $encryptedContent
340
+     * @param string $iv
341
+     * @return string
342
+     */
343
+    private function concatIV($encryptedContent, $iv) {
344
+        return $encryptedContent . '00iv00' . $iv;
345
+    }
346
+
347
+    /**
348
+     * @param string $encryptedContent
349
+     * @param string $signature
350
+     * @return string
351
+     */
352
+    private function concatSig($encryptedContent, $signature) {
353
+        return $encryptedContent . '00sig00' . $signature;
354
+    }
355
+
356
+    /**
357
+     * Note: This is _NOT_ a padding used for encryption purposes. It is solely
358
+     * used to achieve the PHP stream size. It has _NOTHING_ to do with the
359
+     * encrypted content and is not used in any crypto primitive.
360
+     *
361
+     * @param string $data
362
+     * @return string
363
+     */
364
+    private function addPadding($data) {
365
+        return $data . 'xxx';
366
+    }
367
+
368
+    /**
369
+     * generate password hash used to encrypt the users private key
370
+     *
371
+     * @param string $password
372
+     * @param string $cipher
373
+     * @param string $uid only used for user keys
374
+     * @return string
375
+     */
376
+    protected function generatePasswordHash($password, $cipher, $uid = '') {
377
+        $instanceId = $this->config->getSystemValue('instanceid');
378
+        $instanceSecret = $this->config->getSystemValue('secret');
379
+        $salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
380
+        $keySize = $this->getKeySize($cipher);
381
+
382
+        $hash = hash_pbkdf2(
383
+            'sha256',
384
+            $password,
385
+            $salt,
386
+            100000,
387
+            $keySize,
388
+            true
389
+        );
390
+
391
+        return $hash;
392
+    }
393
+
394
+    /**
395
+     * encrypt private key
396
+     *
397
+     * @param string $privateKey
398
+     * @param string $password
399
+     * @param string $uid for regular users, empty for system keys
400
+     * @return false|string
401
+     */
402
+    public function encryptPrivateKey($privateKey, $password, $uid = '') {
403
+        $cipher = $this->getCipher();
404
+        $hash = $this->generatePasswordHash($password, $cipher, $uid);
405
+        $encryptedKey = $this->symmetricEncryptFileContent(
406
+            $privateKey,
407
+            $hash,
408
+            0,
409
+            0
410
+        );
411
+
412
+        return $encryptedKey;
413
+    }
414
+
415
+    /**
416
+     * @param string $privateKey
417
+     * @param string $password
418
+     * @param string $uid for regular users, empty for system keys
419
+     * @return false|string
420
+     */
421
+    public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
422
+        $header = $this->parseHeader($privateKey);
423
+
424
+        if (isset($header['cipher'])) {
425
+            $cipher = $header['cipher'];
426
+        } else {
427
+            $cipher = $this->getLegacyCipher();
428
+        }
429
+
430
+        if (isset($header['keyFormat'])) {
431
+            $keyFormat = $header['keyFormat'];
432
+        } else {
433
+            $keyFormat = self::LEGACY_KEY_FORMAT;
434
+        }
435
+
436
+        if ($keyFormat === self::DEFAULT_KEY_FORMAT) {
437
+            $password = $this->generatePasswordHash($password, $cipher, $uid);
438
+        }
439
+
440
+        $binaryEncoding = isset($header['encoding']) && $header['encoding'] === self::BINARY_ENCODING_FORMAT;
441
+
442
+        // If we found a header we need to remove it from the key we want to decrypt
443
+        if (!empty($header)) {
444
+            $privateKey = substr($privateKey,
445
+                strpos($privateKey,
446
+                    self::HEADER_END) + strlen(self::HEADER_END));
447
+        }
448
+
449
+        $plainKey = $this->symmetricDecryptFileContent(
450
+            $privateKey,
451
+            $password,
452
+            $cipher,
453
+            0,
454
+            0,
455
+            $binaryEncoding
456
+        );
457
+
458
+        if ($this->isValidPrivateKey($plainKey) === false) {
459
+            return false;
460
+        }
461
+
462
+        return $plainKey;
463
+    }
464
+
465
+    /**
466
+     * check if it is a valid private key
467
+     *
468
+     * @param string $plainKey
469
+     * @return bool
470
+     */
471
+    protected function isValidPrivateKey($plainKey) {
472
+        $res = openssl_get_privatekey($plainKey);
473
+        // TODO: remove resource check one php7.4 is not longer supported
474
+        if (is_resource($res) || (is_object($res) && get_class($res) === 'OpenSSLAsymmetricKey')) {
475
+            $sslInfo = openssl_pkey_get_details($res);
476
+            if (isset($sslInfo['key'])) {
477
+                return true;
478
+            }
479
+        }
480
+
481
+        return false;
482
+    }
483
+
484
+    /**
485
+     * @param string $keyFileContents
486
+     * @param string $passPhrase
487
+     * @param string $cipher
488
+     * @param int $version
489
+     * @param int|string $position
490
+     * @param boolean $binaryEncoding
491
+     * @return string
492
+     * @throws DecryptionFailedException
493
+     */
494
+    public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0, bool $binaryEncoding = false) {
495
+        if ($keyFileContents == '') {
496
+            return '';
497
+        }
498
+
499
+        $catFile = $this->splitMetaData($keyFileContents, $cipher);
500
+
501
+        if ($catFile['signature'] !== false) {
502
+            try {
503
+                // First try the new format
504
+                $this->checkSignature($catFile['encrypted'], $passPhrase . '_' . $version . '_' . $position, $catFile['signature']);
505
+            } catch (GenericEncryptionException $e) {
506
+                // For compatibility with old files check the version without _
507
+                $this->checkSignature($catFile['encrypted'], $passPhrase . $version . $position, $catFile['signature']);
508
+            }
509
+        }
510
+
511
+        return $this->decrypt($catFile['encrypted'],
512
+            $catFile['iv'],
513
+            $passPhrase,
514
+            $cipher,
515
+            $binaryEncoding);
516
+    }
517
+
518
+    /**
519
+     * check for valid signature
520
+     *
521
+     * @param string $data
522
+     * @param string $passPhrase
523
+     * @param string $expectedSignature
524
+     * @throws GenericEncryptionException
525
+     */
526
+    private function checkSignature($data, $passPhrase, $expectedSignature) {
527
+        $enforceSignature = !$this->config->getSystemValueBool('encryption_skip_signature_check', false);
528
+
529
+        $signature = $this->createSignature($data, $passPhrase);
530
+        $isCorrectHash = hash_equals($expectedSignature, $signature);
531
+
532
+        if (!$isCorrectHash && $enforceSignature) {
533
+            throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
534
+        } elseif (!$isCorrectHash && !$enforceSignature) {
535
+            $this->logger->info("Signature check skipped", ['app' => 'encryption']);
536
+        }
537
+    }
538
+
539
+    /**
540
+     * create signature
541
+     *
542
+     * @param string $data
543
+     * @param string $passPhrase
544
+     * @return string
545
+     */
546
+    private function createSignature($data, $passPhrase) {
547
+        $passPhrase = hash('sha512', $passPhrase . 'a', true);
548
+        return hash_hmac('sha256', $data, $passPhrase);
549
+    }
550
+
551
+
552
+    /**
553
+     * remove padding
554
+     *
555
+     * @param string $padded
556
+     * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
557
+     * @return string|false
558
+     */
559
+    private function removePadding($padded, $hasSignature = false) {
560
+        if ($hasSignature === false && substr($padded, -2) === 'xx') {
561
+            return substr($padded, 0, -2);
562
+        } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
563
+            return substr($padded, 0, -3);
564
+        }
565
+        return false;
566
+    }
567
+
568
+    /**
569
+     * split meta data from encrypted file
570
+     * Note: for now, we assume that the meta data always start with the iv
571
+     *       followed by the signature, if available
572
+     *
573
+     * @param string $catFile
574
+     * @param string $cipher
575
+     * @return array
576
+     */
577
+    private function splitMetaData($catFile, $cipher) {
578
+        if ($this->hasSignature($catFile, $cipher)) {
579
+            $catFile = $this->removePadding($catFile, true);
580
+            $meta = substr($catFile, -93);
581
+            $iv = substr($meta, strlen('00iv00'), 16);
582
+            $sig = substr($meta, 22 + strlen('00sig00'));
583
+            $encrypted = substr($catFile, 0, -93);
584
+        } else {
585
+            $catFile = $this->removePadding($catFile);
586
+            $meta = substr($catFile, -22);
587
+            $iv = substr($meta, -16);
588
+            $sig = false;
589
+            $encrypted = substr($catFile, 0, -22);
590
+        }
591
+
592
+        return [
593
+            'encrypted' => $encrypted,
594
+            'iv' => $iv,
595
+            'signature' => $sig
596
+        ];
597
+    }
598
+
599
+    /**
600
+     * check if encrypted block is signed
601
+     *
602
+     * @param string $catFile
603
+     * @param string $cipher
604
+     * @return bool
605
+     * @throws GenericEncryptionException
606
+     */
607
+    private function hasSignature($catFile, $cipher) {
608
+        $skipSignatureCheck = $this->config->getSystemValueBool('encryption_skip_signature_check', false);
609
+
610
+        $meta = substr($catFile, -93);
611
+        $signaturePosition = strpos($meta, '00sig00');
612
+
613
+        // If we no longer support the legacy format then everything needs a signature
614
+        if (!$skipSignatureCheck && !$this->supportLegacy && $signaturePosition === false) {
615
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
616
+        }
617
+
618
+        // Enforce signature for the new 'CTR' ciphers
619
+        if (!$skipSignatureCheck && $signaturePosition === false && stripos($cipher, 'ctr') !== false) {
620
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
621
+        }
622
+
623
+        return ($signaturePosition !== false);
624
+    }
625
+
626
+
627
+    /**
628
+     * @param string $encryptedContent
629
+     * @param string $iv
630
+     * @param string $passPhrase
631
+     * @param string $cipher
632
+     * @param boolean $binaryEncoding
633
+     * @return string
634
+     * @throws DecryptionFailedException
635
+     */
636
+    private function decrypt(string $encryptedContent, string $iv, string $passPhrase = '', string $cipher = self::DEFAULT_CIPHER, bool $binaryEncoding = false): string {
637
+        $options = $binaryEncoding === true ? OPENSSL_RAW_DATA : 0;
638
+        $plainContent = openssl_decrypt($encryptedContent,
639
+            $cipher,
640
+            $passPhrase,
641
+            $options,
642
+            $iv);
643
+
644
+        if ($plainContent) {
645
+            return $plainContent;
646
+        } else {
647
+            throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
648
+        }
649
+    }
650
+
651
+    /**
652
+     * @param string $data
653
+     * @return array
654
+     */
655
+    protected function parseHeader($data) {
656
+        $result = [];
657
+
658
+        if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
659
+            $endAt = strpos($data, self::HEADER_END);
660
+            $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
661
+
662
+            // +1 not to start with an ':' which would result in empty element at the beginning
663
+            $exploded = explode(':',
664
+                substr($header, strlen(self::HEADER_START) + 1));
665
+
666
+            $element = array_shift($exploded);
667
+
668
+            while ($element !== self::HEADER_END) {
669
+                $result[$element] = array_shift($exploded);
670
+                $element = array_shift($exploded);
671
+            }
672
+        }
673
+
674
+        return $result;
675
+    }
676
+
677
+    /**
678
+     * generate initialization vector
679
+     *
680
+     * @return string
681
+     * @throws GenericEncryptionException
682
+     */
683
+    private function generateIv() {
684
+        return random_bytes(16);
685
+    }
686
+
687
+    /**
688
+     * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
689
+     * as file key
690
+     *
691
+     * @return string
692
+     * @throws \Exception
693
+     */
694
+    public function generateFileKey() {
695
+        return random_bytes(32);
696
+    }
697
+
698
+    /**
699
+     * @param $encKeyFile
700
+     * @param $shareKey
701
+     * @param $privateKey
702
+     * @return string
703
+     * @throws MultiKeyDecryptException
704
+     */
705
+    public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
706
+        if (!$encKeyFile) {
707
+            throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
708
+        }
709
+
710
+        if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) {
711
+            return $plainContent;
712
+        } else {
713
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
714
+        }
715
+    }
716
+
717
+    /**
718
+     * @param string $plainContent
719
+     * @param array $keyFiles
720
+     * @return array
721
+     * @throws MultiKeyEncryptException
722
+     */
723
+    public function multiKeyEncrypt($plainContent, array $keyFiles) {
724
+        // openssl_seal returns false without errors if plaincontent is empty
725
+        // so trigger our own error
726
+        if (empty($plainContent)) {
727
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
728
+        }
729
+
730
+        // Set empty vars to be set by openssl by reference
731
+        $sealed = '';
732
+        $shareKeys = [];
733
+        $mappedShareKeys = [];
734
+
735
+        if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) {
736
+            $i = 0;
737
+
738
+            // Ensure each shareKey is labelled with its corresponding key id
739
+            foreach ($keyFiles as $userId => $publicKey) {
740
+                $mappedShareKeys[$userId] = $shareKeys[$i];
741
+                $i++;
742
+            }
743
+
744
+            return [
745
+                'keys' => $mappedShareKeys,
746
+                'data' => $sealed
747
+            ];
748
+        } else {
749
+            throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
750
+        }
751
+    }
752
+
753
+    public function useLegacyBase64Encoding(): bool {
754
+        return $this->useLegacyBase64Encoding;
755
+    }
756 756
 }
Please login to merge, or discard this patch.
apps/files_external/lib/Lib/Storage/SFTPReadStream.php 1 patch
Indentation   +170 added lines, -170 removed lines patch added patch discarded remove patch
@@ -30,174 +30,174 @@
 block discarded – undo
30 30
 use phpseclib\Net\SSH2;
31 31
 
32 32
 class SFTPReadStream implements File {
33
-	/** @var resource */
34
-	public $context;
35
-
36
-	/** @var \phpseclib\Net\SFTP */
37
-	private $sftp;
38
-
39
-	/** @var string */
40
-	private $handle;
41
-
42
-	/** @var int */
43
-	private $internalPosition = 0;
44
-
45
-	/** @var int */
46
-	private $readPosition = 0;
47
-
48
-	/** @var bool */
49
-	private $eof = false;
50
-
51
-	private $buffer = '';
52
-
53
-	public static function register($protocol = 'sftpread') {
54
-		if (in_array($protocol, stream_get_wrappers(), true)) {
55
-			return false;
56
-		}
57
-		return stream_wrapper_register($protocol, get_called_class());
58
-	}
59
-
60
-	/**
61
-	 * Load the source from the stream context and return the context options
62
-	 *
63
-	 * @param string $name
64
-	 * @throws \BadMethodCallException
65
-	 */
66
-	protected function loadContext($name) {
67
-		$context = stream_context_get_options($this->context);
68
-		if (isset($context[$name])) {
69
-			$context = $context[$name];
70
-		} else {
71
-			throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
72
-		}
73
-		if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) {
74
-			$this->sftp = $context['session'];
75
-		} else {
76
-			throw new \BadMethodCallException('Invalid context, session not set');
77
-		}
78
-		return $context;
79
-	}
80
-
81
-	public function stream_open($path, $mode, $options, &$opened_path) {
82
-		[, $path] = explode('://', $path);
83
-		$path = '/' . ltrim($path);
84
-		$path = str_replace('//', '/', $path);
85
-
86
-		$this->loadContext('sftp');
87
-
88
-		if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) {
89
-			return false;
90
-		}
91
-
92
-		$remote_file = $this->sftp->_realpath($path);
93
-		if ($remote_file === false) {
94
-			return false;
95
-		}
96
-
97
-		$packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0);
98
-		if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
99
-			return false;
100
-		}
101
-
102
-		$response = $this->sftp->_get_sftp_packet();
103
-		switch ($this->sftp->packet_type) {
104
-			case NET_SFTP_HANDLE:
105
-				$this->handle = substr($response, 4);
106
-				break;
107
-			case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
108
-				$this->sftp->_logError($response);
109
-				return false;
110
-			default:
111
-				user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
112
-				return false;
113
-		}
114
-
115
-		$this->request_chunk(256 * 1024);
116
-
117
-		return true;
118
-	}
119
-
120
-	public function stream_seek($offset, $whence = SEEK_SET) {
121
-		return false;
122
-	}
123
-
124
-	public function stream_tell() {
125
-		return $this->readPosition;
126
-	}
127
-
128
-	public function stream_read($count) {
129
-		if (!$this->eof && strlen($this->buffer) < $count) {
130
-			$chunk = $this->read_chunk();
131
-			$this->buffer .= $chunk;
132
-			if (!$this->eof) {
133
-				$this->request_chunk(256 * 1024);
134
-			}
135
-		}
136
-
137
-		$data = substr($this->buffer, 0, $count);
138
-		$this->buffer = substr($this->buffer, $count);
139
-		$this->readPosition += strlen($data);
140
-
141
-		return $data;
142
-	}
143
-
144
-	private function request_chunk($size) {
145
-		$packet = pack('Na*N3', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size);
146
-		return $this->sftp->_send_sftp_packet(NET_SFTP_READ, $packet);
147
-	}
148
-
149
-	private function read_chunk() {
150
-		$response = $this->sftp->_get_sftp_packet();
151
-
152
-		switch ($this->sftp->packet_type) {
153
-			case NET_SFTP_DATA:
154
-				$temp = substr($response, 4);
155
-				$len = strlen($temp);
156
-				$this->internalPosition += $len;
157
-				return $temp;
158
-			case NET_SFTP_STATUS:
159
-				[1 => $status] = unpack('N', substr($response, 0, 4));
160
-				if ($status == NET_SFTP_STATUS_EOF) {
161
-					$this->eof = true;
162
-				}
163
-				return '';
164
-			default:
165
-				return '';
166
-		}
167
-	}
168
-
169
-	public function stream_write($data) {
170
-		return false;
171
-	}
172
-
173
-	public function stream_set_option($option, $arg1, $arg2) {
174
-		return false;
175
-	}
176
-
177
-	public function stream_truncate($size) {
178
-		return false;
179
-	}
180
-
181
-	public function stream_stat() {
182
-		return false;
183
-	}
184
-
185
-	public function stream_lock($operation) {
186
-		return false;
187
-	}
188
-
189
-	public function stream_flush() {
190
-		return false;
191
-	}
192
-
193
-	public function stream_eof() {
194
-		return $this->eof;
195
-	}
196
-
197
-	public function stream_close() {
198
-		if (!$this->sftp->_close_handle($this->handle)) {
199
-			return false;
200
-		}
201
-		return true;
202
-	}
33
+    /** @var resource */
34
+    public $context;
35
+
36
+    /** @var \phpseclib\Net\SFTP */
37
+    private $sftp;
38
+
39
+    /** @var string */
40
+    private $handle;
41
+
42
+    /** @var int */
43
+    private $internalPosition = 0;
44
+
45
+    /** @var int */
46
+    private $readPosition = 0;
47
+
48
+    /** @var bool */
49
+    private $eof = false;
50
+
51
+    private $buffer = '';
52
+
53
+    public static function register($protocol = 'sftpread') {
54
+        if (in_array($protocol, stream_get_wrappers(), true)) {
55
+            return false;
56
+        }
57
+        return stream_wrapper_register($protocol, get_called_class());
58
+    }
59
+
60
+    /**
61
+     * Load the source from the stream context and return the context options
62
+     *
63
+     * @param string $name
64
+     * @throws \BadMethodCallException
65
+     */
66
+    protected function loadContext($name) {
67
+        $context = stream_context_get_options($this->context);
68
+        if (isset($context[$name])) {
69
+            $context = $context[$name];
70
+        } else {
71
+            throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
72
+        }
73
+        if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) {
74
+            $this->sftp = $context['session'];
75
+        } else {
76
+            throw new \BadMethodCallException('Invalid context, session not set');
77
+        }
78
+        return $context;
79
+    }
80
+
81
+    public function stream_open($path, $mode, $options, &$opened_path) {
82
+        [, $path] = explode('://', $path);
83
+        $path = '/' . ltrim($path);
84
+        $path = str_replace('//', '/', $path);
85
+
86
+        $this->loadContext('sftp');
87
+
88
+        if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) {
89
+            return false;
90
+        }
91
+
92
+        $remote_file = $this->sftp->_realpath($path);
93
+        if ($remote_file === false) {
94
+            return false;
95
+        }
96
+
97
+        $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0);
98
+        if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
99
+            return false;
100
+        }
101
+
102
+        $response = $this->sftp->_get_sftp_packet();
103
+        switch ($this->sftp->packet_type) {
104
+            case NET_SFTP_HANDLE:
105
+                $this->handle = substr($response, 4);
106
+                break;
107
+            case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
108
+                $this->sftp->_logError($response);
109
+                return false;
110
+            default:
111
+                user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
112
+                return false;
113
+        }
114
+
115
+        $this->request_chunk(256 * 1024);
116
+
117
+        return true;
118
+    }
119
+
120
+    public function stream_seek($offset, $whence = SEEK_SET) {
121
+        return false;
122
+    }
123
+
124
+    public function stream_tell() {
125
+        return $this->readPosition;
126
+    }
127
+
128
+    public function stream_read($count) {
129
+        if (!$this->eof && strlen($this->buffer) < $count) {
130
+            $chunk = $this->read_chunk();
131
+            $this->buffer .= $chunk;
132
+            if (!$this->eof) {
133
+                $this->request_chunk(256 * 1024);
134
+            }
135
+        }
136
+
137
+        $data = substr($this->buffer, 0, $count);
138
+        $this->buffer = substr($this->buffer, $count);
139
+        $this->readPosition += strlen($data);
140
+
141
+        return $data;
142
+    }
143
+
144
+    private function request_chunk($size) {
145
+        $packet = pack('Na*N3', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size);
146
+        return $this->sftp->_send_sftp_packet(NET_SFTP_READ, $packet);
147
+    }
148
+
149
+    private function read_chunk() {
150
+        $response = $this->sftp->_get_sftp_packet();
151
+
152
+        switch ($this->sftp->packet_type) {
153
+            case NET_SFTP_DATA:
154
+                $temp = substr($response, 4);
155
+                $len = strlen($temp);
156
+                $this->internalPosition += $len;
157
+                return $temp;
158
+            case NET_SFTP_STATUS:
159
+                [1 => $status] = unpack('N', substr($response, 0, 4));
160
+                if ($status == NET_SFTP_STATUS_EOF) {
161
+                    $this->eof = true;
162
+                }
163
+                return '';
164
+            default:
165
+                return '';
166
+        }
167
+    }
168
+
169
+    public function stream_write($data) {
170
+        return false;
171
+    }
172
+
173
+    public function stream_set_option($option, $arg1, $arg2) {
174
+        return false;
175
+    }
176
+
177
+    public function stream_truncate($size) {
178
+        return false;
179
+    }
180
+
181
+    public function stream_stat() {
182
+        return false;
183
+    }
184
+
185
+    public function stream_lock($operation) {
186
+        return false;
187
+    }
188
+
189
+    public function stream_flush() {
190
+        return false;
191
+    }
192
+
193
+    public function stream_eof() {
194
+        return $this->eof;
195
+    }
196
+
197
+    public function stream_close() {
198
+        if (!$this->sftp->_close_handle($this->handle)) {
199
+            return false;
200
+        }
201
+        return true;
202
+    }
203 203
 }
Please login to merge, or discard this patch.
core/Command/Log/Manage.php 1 patch
Indentation   +167 added lines, -167 removed lines patch added patch discarded remove patch
@@ -35,171 +35,171 @@
 block discarded – undo
35 35
 use Symfony\Component\Console\Output\OutputInterface;
36 36
 
37 37
 class Manage extends Command implements CompletionAwareInterface {
38
-	public const DEFAULT_BACKEND = 'file';
39
-	public const DEFAULT_LOG_LEVEL = 2;
40
-	public const DEFAULT_TIMEZONE = 'UTC';
41
-
42
-	protected IConfig $config;
43
-
44
-	public function __construct(IConfig $config) {
45
-		$this->config = $config;
46
-		parent::__construct();
47
-	}
48
-
49
-	protected function configure() {
50
-		$this
51
-			->setName('log:manage')
52
-			->setDescription('manage logging configuration')
53
-			->addOption(
54
-				'backend',
55
-				null,
56
-				InputOption::VALUE_REQUIRED,
57
-				'set the logging backend [file, syslog, errorlog, systemd]'
58
-			)
59
-			->addOption(
60
-				'level',
61
-				null,
62
-				InputOption::VALUE_REQUIRED,
63
-				'set the log level [debug, info, warning, error, fatal]'
64
-			)
65
-			->addOption(
66
-				'timezone',
67
-				null,
68
-				InputOption::VALUE_REQUIRED,
69
-				'set the logging timezone'
70
-			)
71
-		;
72
-	}
73
-
74
-	protected function execute(InputInterface $input, OutputInterface $output): int {
75
-		// collate config setting to the end, to avoid partial configuration
76
-		$toBeSet = [];
77
-
78
-		if ($backend = $input->getOption('backend')) {
79
-			$this->validateBackend($backend);
80
-			$toBeSet['log_type'] = $backend;
81
-		}
82
-
83
-		$level = $input->getOption('level');
84
-		if ($level !== null) {
85
-			if (is_numeric($level)) {
86
-				$levelNum = $level;
87
-				// sanity check
88
-				$this->convertLevelNumber($levelNum);
89
-			} else {
90
-				$levelNum = $this->convertLevelString($level);
91
-			}
92
-			$toBeSet['loglevel'] = $levelNum;
93
-		}
94
-
95
-		if ($timezone = $input->getOption('timezone')) {
96
-			$this->validateTimezone($timezone);
97
-			$toBeSet['logtimezone'] = $timezone;
98
-		}
99
-
100
-		// set config
101
-		foreach ($toBeSet as $option => $value) {
102
-			$this->config->setSystemValue($option, $value);
103
-		}
104
-
105
-		// display configuration
106
-		$backend = $this->config->getSystemValue('log_type', self::DEFAULT_BACKEND);
107
-		$output->writeln('Enabled logging backend: '.$backend);
108
-
109
-		$levelNum = $this->config->getSystemValue('loglevel', self::DEFAULT_LOG_LEVEL);
110
-		$level = $this->convertLevelNumber($levelNum);
111
-		$output->writeln('Log level: '.$level.' ('.$levelNum.')');
112
-
113
-		$timezone = $this->config->getSystemValue('logtimezone', self::DEFAULT_TIMEZONE);
114
-		$output->writeln('Log timezone: '.$timezone);
115
-		return 0;
116
-	}
117
-
118
-	/**
119
-	 * @param string $backend
120
-	 * @throws \InvalidArgumentException
121
-	 */
122
-	protected function validateBackend($backend) {
123
-		if (!class_exists('OC\\Log\\'.ucfirst($backend))) {
124
-			throw new \InvalidArgumentException('Invalid backend');
125
-		}
126
-	}
127
-
128
-	/**
129
-	 * @param string $timezone
130
-	 * @throws \Exception
131
-	 */
132
-	protected function validateTimezone($timezone) {
133
-		new \DateTimeZone($timezone);
134
-	}
135
-
136
-	/**
137
-	 * @param string $level
138
-	 * @return int
139
-	 * @throws \InvalidArgumentException
140
-	 */
141
-	protected function convertLevelString($level) {
142
-		$level = strtolower($level);
143
-		switch ($level) {
144
-			case 'debug':
145
-				return 0;
146
-			case 'info':
147
-				return 1;
148
-			case 'warning':
149
-			case 'warn':
150
-				return 2;
151
-			case 'error':
152
-			case 'err':
153
-				return 3;
154
-			case 'fatal':
155
-				return 4;
156
-		}
157
-		throw new \InvalidArgumentException('Invalid log level string');
158
-	}
159
-
160
-	/**
161
-	 * @param int $levelNum
162
-	 * @return string
163
-	 * @throws \InvalidArgumentException
164
-	 */
165
-	protected function convertLevelNumber($levelNum) {
166
-		switch ($levelNum) {
167
-			case 0:
168
-				return 'Debug';
169
-			case 1:
170
-				return 'Info';
171
-			case 2:
172
-				return 'Warning';
173
-			case 3:
174
-				return 'Error';
175
-			case 4:
176
-				return 'Fatal';
177
-		}
178
-		throw new \InvalidArgumentException('Invalid log level number');
179
-	}
180
-
181
-	/**
182
-	 * @param string $optionName
183
-	 * @param CompletionContext $context
184
-	 * @return string[]
185
-	 */
186
-	public function completeOptionValues($optionName, CompletionContext $context) {
187
-		if ($optionName === 'backend') {
188
-			return ['file', 'syslog', 'errorlog', 'systemd'];
189
-		} elseif ($optionName === 'level') {
190
-			return ['debug', 'info', 'warning', 'error', 'fatal'];
191
-		} elseif ($optionName === 'timezone') {
192
-			return \DateTimeZone::listIdentifiers();
193
-		}
194
-		return [];
195
-	}
196
-
197
-	/**
198
-	 * @param string $argumentName
199
-	 * @param CompletionContext $context
200
-	 * @return string[]
201
-	 */
202
-	public function completeArgumentValues($argumentName, CompletionContext $context) {
203
-		return [];
204
-	}
38
+    public const DEFAULT_BACKEND = 'file';
39
+    public const DEFAULT_LOG_LEVEL = 2;
40
+    public const DEFAULT_TIMEZONE = 'UTC';
41
+
42
+    protected IConfig $config;
43
+
44
+    public function __construct(IConfig $config) {
45
+        $this->config = $config;
46
+        parent::__construct();
47
+    }
48
+
49
+    protected function configure() {
50
+        $this
51
+            ->setName('log:manage')
52
+            ->setDescription('manage logging configuration')
53
+            ->addOption(
54
+                'backend',
55
+                null,
56
+                InputOption::VALUE_REQUIRED,
57
+                'set the logging backend [file, syslog, errorlog, systemd]'
58
+            )
59
+            ->addOption(
60
+                'level',
61
+                null,
62
+                InputOption::VALUE_REQUIRED,
63
+                'set the log level [debug, info, warning, error, fatal]'
64
+            )
65
+            ->addOption(
66
+                'timezone',
67
+                null,
68
+                InputOption::VALUE_REQUIRED,
69
+                'set the logging timezone'
70
+            )
71
+        ;
72
+    }
73
+
74
+    protected function execute(InputInterface $input, OutputInterface $output): int {
75
+        // collate config setting to the end, to avoid partial configuration
76
+        $toBeSet = [];
77
+
78
+        if ($backend = $input->getOption('backend')) {
79
+            $this->validateBackend($backend);
80
+            $toBeSet['log_type'] = $backend;
81
+        }
82
+
83
+        $level = $input->getOption('level');
84
+        if ($level !== null) {
85
+            if (is_numeric($level)) {
86
+                $levelNum = $level;
87
+                // sanity check
88
+                $this->convertLevelNumber($levelNum);
89
+            } else {
90
+                $levelNum = $this->convertLevelString($level);
91
+            }
92
+            $toBeSet['loglevel'] = $levelNum;
93
+        }
94
+
95
+        if ($timezone = $input->getOption('timezone')) {
96
+            $this->validateTimezone($timezone);
97
+            $toBeSet['logtimezone'] = $timezone;
98
+        }
99
+
100
+        // set config
101
+        foreach ($toBeSet as $option => $value) {
102
+            $this->config->setSystemValue($option, $value);
103
+        }
104
+
105
+        // display configuration
106
+        $backend = $this->config->getSystemValue('log_type', self::DEFAULT_BACKEND);
107
+        $output->writeln('Enabled logging backend: '.$backend);
108
+
109
+        $levelNum = $this->config->getSystemValue('loglevel', self::DEFAULT_LOG_LEVEL);
110
+        $level = $this->convertLevelNumber($levelNum);
111
+        $output->writeln('Log level: '.$level.' ('.$levelNum.')');
112
+
113
+        $timezone = $this->config->getSystemValue('logtimezone', self::DEFAULT_TIMEZONE);
114
+        $output->writeln('Log timezone: '.$timezone);
115
+        return 0;
116
+    }
117
+
118
+    /**
119
+     * @param string $backend
120
+     * @throws \InvalidArgumentException
121
+     */
122
+    protected function validateBackend($backend) {
123
+        if (!class_exists('OC\\Log\\'.ucfirst($backend))) {
124
+            throw new \InvalidArgumentException('Invalid backend');
125
+        }
126
+    }
127
+
128
+    /**
129
+     * @param string $timezone
130
+     * @throws \Exception
131
+     */
132
+    protected function validateTimezone($timezone) {
133
+        new \DateTimeZone($timezone);
134
+    }
135
+
136
+    /**
137
+     * @param string $level
138
+     * @return int
139
+     * @throws \InvalidArgumentException
140
+     */
141
+    protected function convertLevelString($level) {
142
+        $level = strtolower($level);
143
+        switch ($level) {
144
+            case 'debug':
145
+                return 0;
146
+            case 'info':
147
+                return 1;
148
+            case 'warning':
149
+            case 'warn':
150
+                return 2;
151
+            case 'error':
152
+            case 'err':
153
+                return 3;
154
+            case 'fatal':
155
+                return 4;
156
+        }
157
+        throw new \InvalidArgumentException('Invalid log level string');
158
+    }
159
+
160
+    /**
161
+     * @param int $levelNum
162
+     * @return string
163
+     * @throws \InvalidArgumentException
164
+     */
165
+    protected function convertLevelNumber($levelNum) {
166
+        switch ($levelNum) {
167
+            case 0:
168
+                return 'Debug';
169
+            case 1:
170
+                return 'Info';
171
+            case 2:
172
+                return 'Warning';
173
+            case 3:
174
+                return 'Error';
175
+            case 4:
176
+                return 'Fatal';
177
+        }
178
+        throw new \InvalidArgumentException('Invalid log level number');
179
+    }
180
+
181
+    /**
182
+     * @param string $optionName
183
+     * @param CompletionContext $context
184
+     * @return string[]
185
+     */
186
+    public function completeOptionValues($optionName, CompletionContext $context) {
187
+        if ($optionName === 'backend') {
188
+            return ['file', 'syslog', 'errorlog', 'systemd'];
189
+        } elseif ($optionName === 'level') {
190
+            return ['debug', 'info', 'warning', 'error', 'fatal'];
191
+        } elseif ($optionName === 'timezone') {
192
+            return \DateTimeZone::listIdentifiers();
193
+        }
194
+        return [];
195
+    }
196
+
197
+    /**
198
+     * @param string $argumentName
199
+     * @param CompletionContext $context
200
+     * @return string[]
201
+     */
202
+    public function completeArgumentValues($argumentName, CompletionContext $context) {
203
+        return [];
204
+    }
205 205
 }
Please login to merge, or discard this patch.