diff --git a/lib/DigestSender.php b/lib/DigestSender.php index 0dfe4d380..f2bc54977 100644 --- a/lib/DigestSender.php +++ b/lib/DigestSender.php @@ -188,7 +188,7 @@ public function sendDigestForUser(IUser $user, int $now, string $timezone, strin $andMoreText = $l10n->n('and %n more…', 'and %n more…', $skippedCount); $url = $this->urlGenerator->linkToRouteAbsolute('activity.Activities.showList', [ 'filter' => 'all' ]); $template->addBodyListItem( - '' . htmlspecialchars($andMoreText) . '', + '' . htmlspecialchars($andMoreText) . '', plainText: $andMoreText, ); } @@ -235,7 +235,7 @@ protected function getHTMLSubject(IEvent $event): string { } if (isset($parameter['link'])) { - $replacements[] = '' . htmlspecialchars($replacement) . ''; + $replacements[] = '' . htmlspecialchars($replacement) . ''; } else { $replacements[] = '' . htmlspecialchars($replacement) . ''; } diff --git a/lib/MailQueueHandler.php b/lib/MailQueueHandler.php index 46be003e4..ae6087175 100644 --- a/lib/MailQueueHandler.php +++ b/lib/MailQueueHandler.php @@ -331,7 +331,7 @@ function ($event) use ($timezone, $l) { $template->addHeader(); $template->addHeading($l->t('Hello %s', [$user->getDisplayName()]), $l->t('Hello %s,', [$user->getDisplayName()])); - $homeLink = '' . htmlspecialchars($this->getSenderData('name')) . ''; + $homeLink = '' . htmlspecialchars($this->getSenderData('name')) . ''; $template->addBodyText( $l->t('There was some activity at %s', [$homeLink]), $l->t('There was some activity at %s', [$this->urlGenerator->getAbsoluteURL('/')]) @@ -394,7 +394,7 @@ protected function getHTMLSubject(IEvent $event): string { } if (isset($parameter['link'])) { - $replacements[] = '' . htmlspecialchars($replacement) . ''; + $replacements[] = '' . htmlspecialchars($replacement) . ''; } else { $replacements[] = '' . htmlspecialchars($replacement) . ''; } diff --git a/tests/DigestSenderTest.php b/tests/DigestSenderTest.php new file mode 100644 index 000000000..c87f33272 --- /dev/null +++ b/tests/DigestSenderTest.php @@ -0,0 +1,83 @@ +digestSender = new DigestSender( + $this->createMock(IConfig::class), + $this->createMock(Data::class), + $this->createMock(UserSettings::class), + $this->createMock(GroupHelper::class), + $this->createMock(IMailer::class), + $this->createMock(IManager::class), + $this->createMock(IUserManager::class), + $this->createMock(IURLGenerator::class), + $this->createMock(Defaults::class), + $this->createMock(IFactory::class), + $this->createMock(IDateTimeFormatter::class), + $this->createMock(LoggerInterface::class), + ); + } + + public static function provideGetHTMLSubjectData(): array { + return [ + 'no rich subject escapes parsed subject' => [ + '', 'Hello ', [], + 'Hello <World>', + ], + 'linked parameter renders anchor' => [ + 'File {file} was shared', '', + ['file' => ['type' => 'file', 'path' => 'photo.jpg', 'name' => 'photo.jpg', 'link' => 'https://cloud.example.com/f/123']], + 'File photo.jpg was shared', + ], + 'double-quote in link is escaped' => [ + 'File {file} was shared', '', + ['file' => ['type' => 'file', 'path' => 'photo.jpg', 'name' => 'photo.jpg', 'link' => 'https://cloud.example.com/f/123"onmouseover="alert(1)']], + 'File photo.jpg was shared', + ], + 'parameter without link uses strong tag' => [ + 'File {file} was shared', '', + ['file' => ['type' => 'file', 'path' => 'photo.jpg', 'name' => 'photo.jpg']], + 'File photo.jpg was shared', + ], + ]; + } + + #[DataProvider('provideGetHTMLSubjectData')] + public function testGetHTMLSubject(string $richSubject, string $parsedSubject, array $richParams, string $expected): void { + $event = $this->createMock(IEvent::class); + $event->method('getRichSubject')->willReturn($richSubject); + $event->method('getParsedSubject')->willReturn($parsedSubject); + $event->method('getRichSubjectParameters')->willReturn($richParams); + + $result = self::invokePrivate($this->digestSender, 'getHTMLSubject', [$event]); + $this->assertSame($expected, $result); + } +} diff --git a/tests/MailQueueHandlerTest.php b/tests/MailQueueHandlerTest.php index 91a658138..61c08537c 100644 --- a/tests/MailQueueHandlerTest.php +++ b/tests/MailQueueHandlerTest.php @@ -375,6 +375,41 @@ public function testSendEmailsDeletesQueueOnSendReturnFalse(): void { * @param int $maxTime * @param string $explain */ + public static function provideGetHTMLSubjectData(): array { + return [ + 'no rich subject escapes parsed subject' => [ + '', 'Hello ', [], + 'Hello <World>', + ], + 'linked parameter renders anchor' => [ + 'File {file} was shared', '', + ['file' => ['type' => 'file', 'path' => 'photo.jpg', 'name' => 'photo.jpg', 'link' => 'https://cloud.example.com/f/123']], + 'File photo.jpg was shared', + ], + 'double-quote in link is escaped' => [ + 'File {file} was shared', '', + ['file' => ['type' => 'file', 'path' => 'photo.jpg', 'name' => 'photo.jpg', 'link' => 'https://cloud.example.com/f/123"onmouseover="alert(1)']], + 'File photo.jpg was shared', + ], + 'parameter without link uses strong tag' => [ + 'File {file} was shared', '', + ['file' => ['type' => 'file', 'path' => 'photo.jpg', 'name' => 'photo.jpg']], + 'File photo.jpg was shared', + ], + ]; + } + + #[DataProvider('provideGetHTMLSubjectData')] + public function testGetHTMLSubject(string $richSubject, string $parsedSubject, array $richParams, string $expected): void { + $event = $this->createMock(IEvent::class); + $event->method('getRichSubject')->willReturn($richSubject); + $event->method('getParsedSubject')->willReturn($parsedSubject); + $event->method('getRichSubjectParameters')->willReturn($richParams); + + $result = self::invokePrivate($this->mailQueueHandler, 'getHTMLSubject', [$event]); + $this->assertSame($expected, $result); + } + protected function assertRemainingMailEntries(array $users, int $maxTime, string $explain): void { foreach ($users as $user) { [$data,] = self::invokePrivate($this->mailQueueHandler, 'getItemsForUser', [$user, $maxTime]);