mirror of
https://github.com/nextcloud/server.git
synced 2026-05-19 08:25:56 -04:00
fix(ImipService): Make sure non-html fields are escaped and html fields are not
Signed-off-by: David Dreschner <david.dreschner@nextcloud.com>
This commit is contained in:
parent
c2e72d3adc
commit
241eeed4e3
2 changed files with 426 additions and 62 deletions
|
|
@ -78,38 +78,53 @@ class IMipService {
|
|||
return $default;
|
||||
}
|
||||
|
||||
private function generateDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
|
||||
$strikethrough = "<span style='text-decoration: line-through'>%s</span><br />%s";
|
||||
if (!isset($vevent->$property)) {
|
||||
return $default;
|
||||
private function getStrikethroughString(?string $oldString, ?string $newValue = null): ?string {
|
||||
if ($oldString === null || $oldString === '') {
|
||||
return null;
|
||||
}
|
||||
$value = $vevent->$property->getValue();
|
||||
$newstring = $value === null ? null : htmlspecialchars($value);
|
||||
if (isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) {
|
||||
$oldstring = $oldVEvent->$property->getValue();
|
||||
return sprintf($strikethrough, htmlspecialchars($oldstring), $newstring);
|
||||
|
||||
$strikethrough = '<span style="text-decoration: line-through">%s</span><br />%s';
|
||||
return sprintf($strikethrough, $oldString, $newValue ?? '');
|
||||
}
|
||||
|
||||
private function generateDiffString(VEvent $vEvent, VEvent $oldVEvent, string $property): ?string {
|
||||
if (!isset($vEvent->$property)) {
|
||||
return null;
|
||||
}
|
||||
return $newstring;
|
||||
|
||||
$newValue = $vEvent->$property->getValue();
|
||||
$newString = $newValue === null ? null : htmlspecialchars($newValue);
|
||||
|
||||
$propertyChanged = isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newString;
|
||||
if ($propertyChanged) {
|
||||
$oldValue = $oldVEvent->$property->getValue();
|
||||
$oldString = htmlspecialchars($oldValue);
|
||||
|
||||
return $this->getStrikethroughString($oldString, $newString);
|
||||
}
|
||||
return $newString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like generateDiffString() but linkifies the property values if they are urls.
|
||||
*/
|
||||
private function generateLinkifiedDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
|
||||
if (!isset($vevent->$property)) {
|
||||
return $default;
|
||||
private function generateLinkifiedDiffString(VEvent $vEvent, VEvent $oldVEvent, string $property): ?string {
|
||||
if (!isset($vEvent->$property)) {
|
||||
return null;
|
||||
}
|
||||
/** @var string|null $newString */
|
||||
$newString = htmlspecialchars($vevent->$property->getValue());
|
||||
$oldString = isset($oldVEvent->$property) ? htmlspecialchars($oldVEvent->$property->getValue()) : null;
|
||||
if ($oldString !== $newString) {
|
||||
return sprintf(
|
||||
"<span style='text-decoration: line-through'>%s</span><br />%s",
|
||||
$this->linkify($oldString) ?? $oldString ?? '',
|
||||
$this->linkify($newString) ?? $newString ?? ''
|
||||
);
|
||||
|
||||
$newValue = $vEvent->$property->getValue();
|
||||
$newString = $this->linkify($newValue) ?? htmlspecialchars($newValue);
|
||||
|
||||
$propertyChanged = isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newValue;
|
||||
if ($propertyChanged) {
|
||||
$oldValue = $oldVEvent->$property->getValue();
|
||||
$oldString = $this->linkify($oldValue) ?? htmlspecialchars($oldValue);
|
||||
|
||||
return $this->getStrikethroughString($oldString, $newString);
|
||||
}
|
||||
return $this->linkify($newString) ?? $newString;
|
||||
|
||||
return $this->getStrikethroughString($newString);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -119,7 +134,15 @@ class IMipService {
|
|||
if ($url === null) {
|
||||
return null;
|
||||
}
|
||||
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
|
||||
|
||||
$isValidLinkUrl
|
||||
= filter_var($url, FILTER_VALIDATE_URL) !== false
|
||||
&& (
|
||||
str_starts_with($url, 'http://')
|
||||
|| str_starts_with($url, 'https://')
|
||||
);
|
||||
|
||||
if (!$isValidLinkUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +155,6 @@ class IMipService {
|
|||
* @return array
|
||||
*/
|
||||
public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array {
|
||||
|
||||
// construct event reader
|
||||
$eventReaderCurrent = new EventReader($vEvent);
|
||||
$eventReaderPrevious = !empty($oldVEvent) ? new EventReader($oldVEvent) : null;
|
||||
|
|
@ -144,22 +166,26 @@ class IMipService {
|
|||
$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
|
||||
}
|
||||
|
||||
$data['meeting_url_html'] = self::readPropertyWithDefault($vEvent, 'URL', $defaultVal);
|
||||
|
||||
if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) {
|
||||
$data['meeting_location_html'] = $locationHtml;
|
||||
}
|
||||
$data['meeting_location_html'] = $this->linkify($data['meeting_location']);
|
||||
$data['meeting_url_html'] = $this->linkify($data['meeting_url']);
|
||||
|
||||
if (!empty($oldVEvent)) {
|
||||
$data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY');
|
||||
$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION');
|
||||
$data['meeting_location_html'] = $this->generateLinkifiedDiffString($vEvent, $oldVEvent, 'LOCATION');
|
||||
|
||||
$oldMeetingUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
|
||||
$oldMeetingUrlAsLink = $this->linkify($oldMeetingUrl);
|
||||
$meetingUrlAsLinkChanged = !empty($oldMeetingUrlAsLink) && $oldMeetingUrlAsLink !== $data['meeting_url_html'];
|
||||
if ($meetingUrlAsLinkChanged) {
|
||||
$data['meeting_url_html'] = $this->getStrikethroughString(htmlspecialchars($oldMeetingUrl), $data['meeting_url_html']);
|
||||
}
|
||||
|
||||
$oldMeetingWhen = $this->generateWhenString($eventReaderPrevious);
|
||||
$data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
|
||||
$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
|
||||
$data['meeting_location_html'] = $this->generateLinkifiedDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
|
||||
|
||||
$oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
|
||||
$data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url'];
|
||||
|
||||
$data['meeting_when_html'] = $oldMeetingWhen !== $data['meeting_when'] ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when']) : $data['meeting_when'];
|
||||
$meetingWhenChanged = $oldMeetingWhen !== $data['meeting_when'];
|
||||
$data['meeting_when_html'] = $meetingWhenChanged
|
||||
? $this->getStrikethroughString($oldMeetingWhen, $data['meeting_when'])
|
||||
: null;
|
||||
}
|
||||
// generate occurring next string
|
||||
if ($eventReaderCurrent->recurs()) {
|
||||
|
|
@ -183,11 +209,8 @@ class IMipService {
|
|||
$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
|
||||
}
|
||||
|
||||
if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) {
|
||||
$data['meeting_location_html'] = $locationHtml;
|
||||
}
|
||||
|
||||
$data['meeting_url_html'] = $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $data['meeting_url']) : '';
|
||||
$data['meeting_location_html'] = $this->linkify($data['meeting_location']);
|
||||
$data['meeting_url_html'] = $this->linkify($data['meeting_url']);
|
||||
|
||||
// generate occurring next string
|
||||
if ($eventReader->recurs()) {
|
||||
|
|
@ -470,7 +493,7 @@ class IMipService {
|
|||
// days of month
|
||||
if ($er->recurringPattern() === 'R') {
|
||||
$days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' '
|
||||
. implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
|
||||
. implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
|
||||
} else {
|
||||
$days = implode(', ', $er->recurringDaysOfMonth());
|
||||
}
|
||||
|
|
@ -537,7 +560,7 @@ class IMipService {
|
|||
// days of month
|
||||
if ($er->recurringPattern() === 'R') {
|
||||
$days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' '
|
||||
. implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
|
||||
. implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
|
||||
} else {
|
||||
$days = $er->startDateTime()->format('jS');
|
||||
}
|
||||
|
|
@ -627,7 +650,6 @@ class IMipService {
|
|||
* @return string
|
||||
*/
|
||||
public function generateOccurringString(EventReader $er): string {
|
||||
|
||||
// initialize
|
||||
$occurrence = null;
|
||||
$occurrence2 = null;
|
||||
|
|
@ -798,26 +820,26 @@ class IMipService {
|
|||
// construct event reader
|
||||
$eventReaderCurrent = new EventReader($vEvent);
|
||||
$defaultVal = '';
|
||||
$strikethrough = "<span style='text-decoration: line-through'>%s</span>";
|
||||
|
||||
$newMeetingWhen = $this->generateWhenString($eventReaderCurrent);
|
||||
$newSummary = htmlspecialchars(isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event'));
|
||||
$newDescription = htmlspecialchars(isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal);
|
||||
$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
|
||||
$newLocation = htmlspecialchars(isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal);
|
||||
$newLocationHtml = $this->linkify($newLocation) ?? $newLocation;
|
||||
$newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');
|
||||
$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
|
||||
$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? $this->linkify((string)$vEvent->URL) : $defaultVal;
|
||||
$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
|
||||
$newLocationHtml = $this->linkify($newLocation);
|
||||
|
||||
$data = [];
|
||||
$data['meeting_when_html'] = $newMeetingWhen === '' ?: sprintf($strikethrough, $newMeetingWhen);
|
||||
$data['meeting_when_html'] = $this->getStrikethroughString(htmlspecialchars($newMeetingWhen));
|
||||
$data['meeting_when'] = $newMeetingWhen;
|
||||
$data['meeting_title_html'] = sprintf($strikethrough, $newSummary);
|
||||
$data['meeting_title_html'] = $this->getStrikethroughString(htmlspecialchars($newSummary));
|
||||
$data['meeting_title'] = $newSummary !== '' ? $newSummary: $this->l10n->t('Untitled event');
|
||||
$data['meeting_description_html'] = $newDescription !== '' ? sprintf($strikethrough, $newDescription) : '';
|
||||
$data['meeting_description_html'] = $this->getStrikethroughString(htmlspecialchars($newDescription));
|
||||
$data['meeting_description'] = $newDescription;
|
||||
$data['meeting_url_html'] = $newUrl !== '' ? sprintf($strikethrough, $newUrl) : '';
|
||||
$data['meeting_url_html'] = $this->getStrikethroughString($newUrl);
|
||||
$data['meeting_url'] = isset($vEvent->URL) ? (string)$vEvent->URL : '';
|
||||
$data['meeting_location_html'] = $newLocationHtml !== '' ? sprintf($strikethrough, $newLocationHtml) : '';
|
||||
$data['meeting_location_html'] = $this->getStrikethroughString($newLocationHtml ?? htmlspecialchars($newLocation));
|
||||
$data['meeting_location'] = $newLocation;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ use OCP\IL10N;
|
|||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\L10N\IFactory;
|
||||
use OCP\Mail\IEMailTemplate;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
|
|
@ -226,6 +227,7 @@ class IMipServiceTest extends TestCase {
|
|||
'meeting_description' => '',
|
||||
'meeting_title' => 'Testing Event',
|
||||
'meeting_location' => '',
|
||||
'meeting_location_html' => '',
|
||||
'meeting_url' => '',
|
||||
'meeting_url_html' => '',
|
||||
];
|
||||
|
|
@ -235,6 +237,64 @@ class IMipServiceTest extends TestCase {
|
|||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testBuildBodyDataCreatedEscapesStrings(): void {
|
||||
$this->l10n->method('l')
|
||||
->willReturnCallback(static function (string $type, \DateTime $date, $_):string {
|
||||
if ($type === 'time') {
|
||||
return $date->format('H:i');
|
||||
}
|
||||
|
||||
return $date->format('m-d');
|
||||
});
|
||||
$this->l10n->method('n')
|
||||
->willReturnCallback(function (string $singular, string $plural, int $count) {
|
||||
if ($count === 1) {
|
||||
return $singular;
|
||||
}
|
||||
|
||||
return $plural;
|
||||
});
|
||||
|
||||
$this->timeFactory->method('getDateTime')->willReturnCallback(
|
||||
function ($v1, $v2) {
|
||||
return match (true) {
|
||||
$v1 == 'now' && $v2 == null => (new \DateTime('20240630T000000'))
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
$testTitle = '<h1>Test event</h1>';
|
||||
$testLocation = '<h1>Stuttgart Office</h1>';
|
||||
$testDescription = '<h1>Description for event</h1>';
|
||||
$testUrl = 'https://example.test/event?id=123"><script>alert(1)</script><a href="';
|
||||
|
||||
$vCalendar = new VCalendar();
|
||||
$vEvent = $vCalendar->add('VEVENT', []);
|
||||
$vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
|
||||
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'Europe/Berlin']);
|
||||
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'Europe/Berlin']);
|
||||
$vEvent->add('SUMMARY', $testTitle);
|
||||
$vEvent->add('LOCATION', $testLocation);
|
||||
$vEvent->add('DESCRIPTION', $testDescription);
|
||||
$vEvent->add('URL', $testUrl);
|
||||
|
||||
$eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
|
||||
|
||||
$expected = [
|
||||
'meeting_when' => $this->service->generateWhenString($eventReader),
|
||||
'meeting_description' => $testDescription,
|
||||
'meeting_title' => $testTitle,
|
||||
'meeting_location' => $testLocation,
|
||||
'meeting_location_html' => null,
|
||||
'meeting_url' => $testUrl,
|
||||
'meeting_url_html' => null,
|
||||
];
|
||||
|
||||
$actual = $this->service->buildBodyData($vCalendar->VEVENT[0], null);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testBuildBodyDataUpdate(): void {
|
||||
|
||||
// construct l10n return(s)
|
||||
|
|
@ -278,11 +338,11 @@ class IMipServiceTest extends TestCase {
|
|||
'meeting_title' => 'Testing Event',
|
||||
'meeting_location' => '',
|
||||
'meeting_url' => '',
|
||||
'meeting_url_html' => '',
|
||||
'meeting_when_html' => $this->service->generateWhenString($eventReaderNew),
|
||||
'meeting_title_html' => sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", 'Testing Singleton Event', 'Testing Event'),
|
||||
'meeting_description_html' => '',
|
||||
'meeting_location_html' => ''
|
||||
'meeting_url_html' => null,
|
||||
'meeting_when_html' => null,
|
||||
'meeting_title_html' => sprintf('<span style="text-decoration: line-through">%s</span><br />%s', 'Testing Singleton Event', 'Testing Event'),
|
||||
'meeting_description_html' => null,
|
||||
'meeting_location_html' => null
|
||||
];
|
||||
// generate actual output
|
||||
$actual = $this->service->buildBodyData($vCalendarNew->VEVENT[0], $vCalendarOld->VEVENT[0]);
|
||||
|
|
@ -290,6 +350,205 @@ class IMipServiceTest extends TestCase {
|
|||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testBuildBodyDataUpdatedEscapesStrings(): void {
|
||||
$this->l10n->method('l')
|
||||
->willReturnCallback(static function (string $type, \DateTime $date):string {
|
||||
if ($type === 'time') {
|
||||
return $date->format('H:i');
|
||||
}
|
||||
|
||||
return $date->format('m-d');
|
||||
});
|
||||
|
||||
$this->l10n->method('n')
|
||||
->willReturnCallback(function (string $singular, string $plural, int $count) {
|
||||
if ($count === 1) {
|
||||
return $singular;
|
||||
}
|
||||
|
||||
return $plural;
|
||||
});
|
||||
|
||||
$this->timeFactory->method('getDateTime')->willReturnCallback(
|
||||
function ($v1, $v2) {
|
||||
return match (true) {
|
||||
$v1 == 'now' && $v2 == null => (new \DateTime('20240601T000000'))
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
$oldTitle = '<h1>Old event</h1>';
|
||||
$oldLocation = '<h1>Stuttgart Office</h1>';
|
||||
$oldDescription = '<h1>Description for old event</h1>';
|
||||
$oldUrl = 'https://example.test/event?id=123"><script>alert(1)</script><a href="';
|
||||
|
||||
$newTitle = '<h1>New event</h1>';
|
||||
$newLocation = '<h1>Berlin Office</h1>';
|
||||
$newDescription = '<h1>Description for new event</h1>';
|
||||
$newUrl = 'https://example.test/event?id=456"><script>alert(1)</script><a href="';
|
||||
|
||||
$vCalendarNew = new VCalendar();
|
||||
$vEventNew = $vCalendarNew->add('VEVENT', []);
|
||||
$vEventNew->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
|
||||
$vEventNew->add('DTSTART', '20240701T080000', ['TZID' => 'Europe/Berlin']);
|
||||
$vEventNew->add('DTEND', '20240701T090000', ['TZID' => 'Europe/Berlin']);
|
||||
$vEventNew->add('SUMMARY', $newTitle);
|
||||
$vEventNew->add('LOCATION', $newLocation);
|
||||
$vEventNew->add('DESCRIPTION', $newDescription);
|
||||
$vEventNew->add('URL', $newUrl);
|
||||
|
||||
$vCalendarOld = new VCalendar();
|
||||
$vEventOld = $vCalendarOld->add('VEVENT', []);
|
||||
$vEventOld->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
|
||||
$vEventOld->add('DTSTART', '20240702T080000', ['TZID' => 'Europe/Berlin']);
|
||||
$vEventOld->add('DTEND', '20240702T090000', ['TZID' => 'Europe/Berlin']);
|
||||
$vEventOld->add('SUMMARY', $oldTitle);
|
||||
$vEventOld->add('LOCATION', $oldLocation);
|
||||
$vEventOld->add('DESCRIPTION', $oldDescription);
|
||||
$vEventOld->add('URL', $oldUrl);
|
||||
|
||||
$eventReaderNew = new EventReader($vCalendarNew, $vCalendarNew->VEVENT[0]->UID->getValue());
|
||||
|
||||
$expected = [
|
||||
'meeting_when' => $this->service->generateWhenString($eventReaderNew),
|
||||
'meeting_when_html' => null,
|
||||
'meeting_description' => $newDescription,
|
||||
'meeting_title' => $newTitle,
|
||||
'meeting_location' => $newLocation,
|
||||
'meeting_url' => $newUrl,
|
||||
'meeting_url_html' => null,
|
||||
'meeting_title_html' => sprintf('<span style="text-decoration: line-through">%s</span><br />%s', htmlspecialchars($oldTitle), htmlspecialchars($newTitle)),
|
||||
'meeting_description_html' => sprintf('<span style="text-decoration: line-through">%s</span><br />%s', htmlspecialchars($oldDescription), htmlspecialchars($newDescription)),
|
||||
'meeting_location_html' => sprintf('<span style="text-decoration: line-through">%s</span><br />%s', htmlspecialchars($oldLocation), htmlspecialchars($newLocation)),
|
||||
];
|
||||
|
||||
$actual = $this->service->buildBodyData($vCalendarNew->VEVENT[0], $vCalendarOld->VEVENT[0]);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testBuildReplyBodyDataEscapesStrings(): void {
|
||||
$this->l10n->method('l')
|
||||
->willReturnCallback(static function (string $type, \DateTime $date, $_):string {
|
||||
if ($type === 'time') {
|
||||
return $date->format('H:i');
|
||||
}
|
||||
|
||||
return $date->format('m-d');
|
||||
});
|
||||
$this->l10n->method('n')
|
||||
->willReturnCallback(function (string $singular, string $plural, int $count) {
|
||||
if ($count === 1) {
|
||||
return $singular;
|
||||
}
|
||||
|
||||
return $plural;
|
||||
});
|
||||
|
||||
$this->timeFactory->method('getDateTime')->willReturnCallback(
|
||||
function ($v1, $v2) {
|
||||
return match (true) {
|
||||
$v1 == 'now' && $v2 == null => (new \DateTime('20240601T000000'))
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
$testTitle = '<h1>Test event</h1>';
|
||||
$testLocation = '<h1>Stuttgart Office</h1>';
|
||||
$testDescription = '<h1>Description for event</h1>';
|
||||
$testUrl = 'https://example.test/event?id=123"><script>alert(1)</script><a href="';
|
||||
|
||||
$vCalendar = new VCalendar();
|
||||
$vEvent = $vCalendar->add('VEVENT', []);
|
||||
$vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
|
||||
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'Europe/Berlin']);
|
||||
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'Europe/Berlin']);
|
||||
$vEvent->add('SUMMARY', $testTitle);
|
||||
$vEvent->add('LOCATION', $testLocation);
|
||||
$vEvent->add('DESCRIPTION', $testDescription);
|
||||
$vEvent->add('URL', $testUrl);
|
||||
|
||||
$eventReaderNew = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
|
||||
|
||||
$expected = [
|
||||
'meeting_when' => $this->service->generateWhenString($eventReaderNew),
|
||||
'meeting_description' => $testDescription,
|
||||
'meeting_title' => $testTitle,
|
||||
'meeting_location' => $testLocation,
|
||||
'meeting_location_html' => null,
|
||||
'meeting_url' => $testUrl,
|
||||
'meeting_url_html' => null,
|
||||
];
|
||||
|
||||
$actual = $this->service->buildReplyBodyData($vCalendar->VEVENT[0]);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testBuildCancelledBodyDataEscapesStrings(): void {
|
||||
$this->l10n->method('l')
|
||||
->willReturnCallback(static function (string $type, \DateTime $date, $_):string {
|
||||
if ($type === 'time') {
|
||||
return $date->format('H:i');
|
||||
}
|
||||
|
||||
return $date->format('m-d');
|
||||
});
|
||||
|
||||
$this->l10n->method('n')
|
||||
->willReturnCallback(function (string $singular, string $plural, int $count) {
|
||||
if ($count === 1) {
|
||||
return $singular;
|
||||
}
|
||||
|
||||
return $plural;
|
||||
});
|
||||
|
||||
$this->timeFactory->method('getDateTime')->willReturnCallback(
|
||||
function ($v1, $v2) {
|
||||
return match (true) {
|
||||
$v1 == 'now' && $v2 == null => (new \DateTime('20240601T000000'))
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
$testTitle = '<h1>Test event</h1>';
|
||||
$testLocation = '<h1>Stuttgart Office</h1>';
|
||||
$testDescription = '<h1>Description for event</h1>';
|
||||
$testUrl = 'https://example.test/event?id=123"><script>alert(1)</script><a href="';
|
||||
|
||||
$vCalendar = new VCalendar();
|
||||
$vEvent = $vCalendar->add('VEVENT', []);
|
||||
$vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
|
||||
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'Europe/Berlin']);
|
||||
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'Europe/Berlin']);
|
||||
$vEvent->add('SUMMARY', $testTitle);
|
||||
$vEvent->add('LOCATION', $testLocation);
|
||||
$vEvent->add('DESCRIPTION', $testDescription);
|
||||
$vEvent->add('URL', $testUrl);
|
||||
|
||||
$eventReaderNew = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue());
|
||||
|
||||
$expectedWhenString = $this->service->generateWhenString($eventReaderNew);
|
||||
|
||||
$expected = [
|
||||
'meeting_when' => $expectedWhenString,
|
||||
'meeting_when_html' => sprintf('<span style="text-decoration: line-through">%s</span><br />', htmlspecialchars($expectedWhenString)),
|
||||
'meeting_description' => $testDescription,
|
||||
'meeting_title' => $testTitle,
|
||||
'meeting_location' => $testLocation,
|
||||
'meeting_url' => $testUrl,
|
||||
'meeting_url_html' => null,
|
||||
'meeting_title_html' => sprintf('<span style="text-decoration: line-through">%s</span><br />', htmlspecialchars($testTitle)),
|
||||
'meeting_description_html' => sprintf('<span style="text-decoration: line-through">%s</span><br />', htmlspecialchars($testDescription)),
|
||||
'meeting_location_html' => sprintf('<span style="text-decoration: line-through">%s</span><br />', htmlspecialchars($testLocation)),
|
||||
];
|
||||
|
||||
$actual = $this->service->buildCancelledBodyData($vCalendar->VEVENT[0]);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testGetLastOccurrenceRRULE(): void {
|
||||
$vCalendar = new VCalendar();
|
||||
$vCalendar->add('VEVENT', [
|
||||
|
|
@ -2231,4 +2490,87 @@ class IMipServiceTest extends TestCase {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that HTML properties are preferred over non-HTML ones
|
||||
* and that these will be passed through as is.
|
||||
*/
|
||||
public function testAddBulletListMaintainsHtmlProperties(): void {
|
||||
$template = $this->createMock(IEMailTemplate::class);
|
||||
$vCalendar = new VCalendar();
|
||||
$vEvent = $vCalendar->add('VEVENT', []);
|
||||
$vEvent->add('SUMMARY', 'Test Event');
|
||||
$vEvent->add('UID', 'test-uid');
|
||||
|
||||
$data = [
|
||||
'meeting_title' => 'Title',
|
||||
'meeting_title_html' => '<strong>Title</strong>',
|
||||
'meeting_when' => 'Monday & Tuesday',
|
||||
'meeting_when_html' => '<em>Monday & Tuesday</em>',
|
||||
'meeting_location' => 'Room A',
|
||||
'meeting_location_html' => '<span>Room <b>A</b></span>',
|
||||
'meeting_url' => 'https://example.com',
|
||||
'meeting_url_html' => '<a href="https://example.com">https://example.com</a>',
|
||||
'meeting_description' => 'Description',
|
||||
'meeting_description_html' => '<p>Description</p>',
|
||||
'meeting_occurring' => 'Every Monday',
|
||||
'meeting_occurring_html' => '<em>Every Monday</em>',
|
||||
];
|
||||
|
||||
$actualHtmlValues = [];
|
||||
$template
|
||||
->method('addBodyListItem')
|
||||
->willReturnCallback(function (string $html) use (&$actualHtmlValues) {
|
||||
$actualHtmlValues[] = $html;
|
||||
});
|
||||
|
||||
$this->appConfig->method('getValueBool')->willReturn(false);
|
||||
$this->service->addBulletList($template, $vEvent, $data);
|
||||
|
||||
$this->assertCount(6, $actualHtmlValues);
|
||||
$this->assertSame($data['meeting_title_html'], $actualHtmlValues[0]);
|
||||
$this->assertSame($data['meeting_when_html'], $actualHtmlValues[1]);
|
||||
$this->assertSame($data['meeting_location_html'], $actualHtmlValues[2]);
|
||||
$this->assertSame($data['meeting_url_html'], $actualHtmlValues[3]);
|
||||
$this->assertSame($data['meeting_occurring_html'], $actualHtmlValues[4]);
|
||||
$this->assertSame($data['meeting_description_html'], $actualHtmlValues[5]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that non-HTML properties are used when no HTML properties
|
||||
* being provided and that those are escaped through `htmlspecialchars()`.
|
||||
*/
|
||||
public function testAddBulletListEscapesNonHtmlProperties(): void {
|
||||
$template = $this->createMock(IEMailTemplate::class);
|
||||
$vCalendar = new VCalendar();
|
||||
$vEvent = $vCalendar->add('VEVENT', []);
|
||||
$vEvent->add('SUMMARY', 'Test Event');
|
||||
$vEvent->add('UID', 'test-uid');
|
||||
|
||||
$data = [
|
||||
'meeting_title' => '<script>alert("xss")</script>',
|
||||
'meeting_when' => '<em>Monday & Tuesday</em>',
|
||||
'meeting_location' => '<div onclick="hack()">Room "A" & B</div>',
|
||||
'meeting_url' => 'https://example.com/?a=1&b=<script>',
|
||||
'meeting_description' => '<p>Description with "quotes" & ampersands</p>',
|
||||
'meeting_occurring' => '<b>Every Monday & Wednesday</b>',
|
||||
];
|
||||
|
||||
$actualHtmlValues = [];
|
||||
$template
|
||||
->method('addBodyListItem')
|
||||
->willReturnCallback(function (string $html) use (&$actualHtmlValues) {
|
||||
$actualHtmlValues[] = $html;
|
||||
});
|
||||
|
||||
$this->appConfig->method('getValueBool')->willReturn(false);
|
||||
$this->service->addBulletList($template, $vEvent, $data);
|
||||
|
||||
$this->assertCount(6, $actualHtmlValues);
|
||||
$this->assertSame(htmlspecialchars($data['meeting_title']), $actualHtmlValues[0]);
|
||||
$this->assertSame(htmlspecialchars($data['meeting_when']), $actualHtmlValues[1]);
|
||||
$this->assertSame(htmlspecialchars($data['meeting_location']), $actualHtmlValues[2]);
|
||||
$this->assertSame(htmlspecialchars($data['meeting_url']), $actualHtmlValues[3]);
|
||||
$this->assertSame(htmlspecialchars($data['meeting_occurring']), $actualHtmlValues[4]);
|
||||
$this->assertSame(htmlspecialchars($data['meeting_description']), $actualHtmlValues[5]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue