diff --git a/lib/Controller/FederatedInvitesController.php b/lib/Controller/FederatedInvitesController.php index 1fdd376c1..193af3c11 100644 --- a/lib/Controller/FederatedInvitesController.php +++ b/lib/Controller/FederatedInvitesController.php @@ -777,18 +777,17 @@ private function sendInvitationEmail(string $token, string $senderProvider, stri $emailTemplate->setSubject($subject); $emailTemplate->addHeader(); - $emailTemplate->addHeading($this->il10->t('%1$s invites you to exchange contact information.', [$initiatorDisplayName]), false); + $emailTemplate->addHeading($this->il10->t('%1$s invites you to exchange contact information.', [$initiatorDisplayName])); if ($isSenderCopy) { $emailTemplate->addBodyText($this->il10->t('This is a copy of the invitation you\'ve sent to %1$s', [$recipientEmail])); } - $emailTemplate->addBodyText($this->il10->t('Hi there,')); - $emailTemplate->addBodyText($this->il10->t('%1$s invites you to exchange cloud accounts and contact information.', [$initiatorDisplayName])); $emailTemplate->addBodyText($this->il10->t('This will allow you to share data with each other.')); if (trim($message) !== '') { - $emailTemplate->addBodyListItem($message, $this->il10->t('Message:')); + $messageLabel = $this->il10->t('Message:'); + $emailTemplate->addBodyListItem($message, $messageLabel, '', '', '', mb_strlen($messageLabel)); } $emailTemplate->addBodyText($this->il10->t('To accept this invite, click the button below and sign in with your cloud provider:')); @@ -796,10 +795,20 @@ private function sendInvitationEmail(string $token, string $senderProvider, stri $this->il10->t('Accept invitation'), $inviteLink ); + $emailTemplate->addBodyText( + htmlspecialchars($this->il10->t('If the button does not work, copy this link into your browser: %1$s', [$inviteLink]), ENT_QUOTES, 'UTF-8'), + false + ); - $emailTemplate->addBodyText($this->il10->t('Invitation details:')); - $emailTemplate->addBodyText($this->il10->t('Invite code: %1$s', ["$token@$senderProvider"])); - $emailTemplate->addBodyText($this->il10->t('Encoded invite: %1$s', [$encoded])); + $details = [ + $this->il10->t('Invitation details:'), + $this->il10->t('Invite code: %1$s', ["$token@$senderProvider"]), + $this->il10->t('Encoded invite: %1$s', [$encoded]), + ]; + $emailTemplate->addBodyText( + '' . implode('
', array_map(static fn (string $line): string => htmlspecialchars($line, ENT_QUOTES, 'UTF-8'), $details)) . '
', + implode(PHP_EOL, $details) + ); $emailTemplate->addFooter(); diff --git a/tests/unit/Controller/FederatedInvitesControllerTest.php b/tests/unit/Controller/FederatedInvitesControllerTest.php index c6b9bf1e1..660174c49 100644 --- a/tests/unit/Controller/FederatedInvitesControllerTest.php +++ b/tests/unit/Controller/FederatedInvitesControllerTest.php @@ -11,7 +11,6 @@ use ChristophWurst\Nextcloud\Testing\TestCase; use OC\App\CompareVersion; -use OC\Mail\EMailTemplate; use OCA\Contacts\Db\FederatedInvite; use OCA\Contacts\Db\FederatedInviteMapper; use OCA\Contacts\Service\FederatedInvitesService; @@ -795,27 +794,44 @@ public function testSetOcmInviteBoolSettingReturnsForbiddenOnUnknownKey(): void } /** - * Renders the controller's IEMailTemplate through the real server implementation - * and captures the resulting bodies when the template is attached to the message. + * Captures the bodies built through IEMailTemplate when the template is attached to the message. + * Text is HTML-escaped only when the plain text falls back to it, as documented by IEMailTemplate. */ private function createTemplateCapturingMessage(?string &$capturedHtml, ?string &$capturedPlain): \OCP\Mail\IMessage|MockObject { - $this->languageFactory->method('get')->willReturn($this->l10n); - $this->mailer->method('createEMailTemplate')->willReturnCallback( - fn (string $emailId, array $data = []): \OCP\Mail\IEMailTemplate => new EMailTemplate( - $this->defaults, - $this->urlGenerator, - $this->languageFactory, - null, - null, - $emailId, - $data, - ) - ); + $html = ''; + $plain = ''; + $append = static function (string $text, string|bool $plainText) use (&$html, &$plain): void { + if ($plainText === '' || $plainText === true) { + $html .= htmlspecialchars($text) . "\n"; + $plain .= $text . "\n"; + return; + } + $html .= $text . "\n"; + if ($plainText !== false) { + $plain .= $plainText . "\n"; + } + }; + + $template = $this->createMock(\OCP\Mail\IEMailTemplate::class); + $template->method('addHeading')->willReturnCallback(static fn (string $title, string|bool $plainTitle = '') => $append($title, $plainTitle)); + $template->method('addBodyText')->willReturnCallback(static fn (string $text, string|bool $plainText = '') => $append($text, $plainText)); + $template->method('addBodyListItem')->willReturnCallback(static function (string $text, string $metaInfo = '', string $icon = '', string|bool $plainText = '', string|bool $plainMetaInfo = '') use ($append): void { + $append($metaInfo, $plainMetaInfo); + $append($text, $plainText); + }); + $template->method('addBodyButton')->willReturnCallback(static function (string $text, string $url, string|bool $plainText = '') use (&$html, &$plain): void { + $html .= htmlspecialchars($text) . ' ' . htmlspecialchars($url) . "\n"; + if ($plainText !== false) { + $plain .= ($plainText === '' ? $text : $plainText) . ': ' . $url . "\n"; + } + }); + $this->mailer->method('createEMailTemplate')->willReturn($template); $message = $this->createMock(\OCP\Mail\IMessage::class); - $message->method('useTemplate')->willReturnCallback(function (\OCP\Mail\IEMailTemplate $template) use (&$capturedHtml, &$capturedPlain, $message) { - $capturedHtml = $template->renderHtml(); - $capturedPlain = $template->renderText(); + $message->method('useTemplate')->willReturnCallback(function (\OCP\Mail\IEMailTemplate $usedTemplate) use (&$capturedHtml, &$capturedPlain, &$html, &$plain, $template, $message) { + $this->assertSame($template, $usedTemplate); + $capturedHtml = $html; + $capturedPlain = $plain; return $message; }); return $message;