handle localization and property parameters in CalDAV iCal parsing (#16519)
Co-authored-by: guillim <guigloo@msn.com>
This commit is contained in:
+13
-4
@@ -13,6 +13,7 @@ import {
|
||||
syncCollection,
|
||||
} from 'tsdav';
|
||||
|
||||
import { icalDataExtractPropertyValue } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/utils/icalDataExtractPropertyValue';
|
||||
import { CalDavGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/services/caldav-get-events.service';
|
||||
import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
||||
import {
|
||||
@@ -269,18 +270,26 @@ export class CalDAVClient {
|
||||
const event = events[0] as ical.VEvent;
|
||||
const participants = this.extractParticipantsFromEvent(event);
|
||||
|
||||
const title = icalDataExtractPropertyValue(
|
||||
event.summary,
|
||||
'Untitled Event',
|
||||
);
|
||||
const description = icalDataExtractPropertyValue(event.description);
|
||||
const location = icalDataExtractPropertyValue(event.location);
|
||||
const conferenceLinkUrl = icalDataExtractPropertyValue(event.url);
|
||||
|
||||
return {
|
||||
id: objectUrl,
|
||||
title: event.summary || 'Untitled Event',
|
||||
title,
|
||||
iCalUid: event.uid || '',
|
||||
description: event.description || '',
|
||||
description,
|
||||
startsAt: event.start.toISOString(),
|
||||
endsAt: event.end.toISOString(),
|
||||
location: event.location || '',
|
||||
location,
|
||||
isFullDay: this.isFullDayEvent(rawData),
|
||||
isCanceled: event.status === 'CANCELLED',
|
||||
conferenceLinkLabel: '',
|
||||
conferenceLinkUrl: event.url,
|
||||
conferenceLinkUrl,
|
||||
externalCreatedAt:
|
||||
event.created?.toISOString() || new Date().toISOString(),
|
||||
externalUpdatedAt:
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { icalDataExtractPropertyValue } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/utils/icalDataExtractPropertyValue';
|
||||
|
||||
describe('icalDataExtractPropertyValue', () => {
|
||||
describe('properties with parameters (RFC 5545 Section 3.2)', () => {
|
||||
it('should extract value from property object with val and params', () => {
|
||||
const property = {
|
||||
val: 'Meeting Title',
|
||||
params: { LANGUAGE: 'en-US' },
|
||||
};
|
||||
|
||||
const result = icalDataExtractPropertyValue(property);
|
||||
|
||||
expect(result).toBe('Meeting Title');
|
||||
});
|
||||
|
||||
it('should handle property with val but no params', () => {
|
||||
const property = {
|
||||
val: 'Conference Room A',
|
||||
};
|
||||
|
||||
const result = icalDataExtractPropertyValue(property);
|
||||
|
||||
expect(result).toBe('Conference Room A');
|
||||
});
|
||||
|
||||
it('should convert non-string val to string', () => {
|
||||
const property = {
|
||||
val: 12345,
|
||||
params: { TYPE: 'INTEGER' },
|
||||
} as any;
|
||||
|
||||
const result = icalDataExtractPropertyValue(property);
|
||||
|
||||
expect(result).toBe('12345');
|
||||
});
|
||||
|
||||
it('should handle property with empty string val', () => {
|
||||
const property = {
|
||||
val: '',
|
||||
params: { LANGUAGE: 'de-DE' },
|
||||
};
|
||||
|
||||
const result = icalDataExtractPropertyValue(property, 'default');
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple values in a single property (RFC 5545 Section 3.1.2)', () => {
|
||||
it('should join multiple string values with comma and space', () => {
|
||||
const property = ['Value 1', 'Value 2', 'Value 3'] as any;
|
||||
|
||||
const result = icalDataExtractPropertyValue(property);
|
||||
|
||||
expect(result).toBe('Value 1, Value 2, Value 3');
|
||||
});
|
||||
|
||||
it('should handle array of property objects with val', () => {
|
||||
const property = [
|
||||
{ val: 'First Value', params: { LANGUAGE: 'en' } },
|
||||
{ val: 'Second Value', params: { LANGUAGE: 'fr' } },
|
||||
] as any;
|
||||
|
||||
const result = icalDataExtractPropertyValue(property);
|
||||
|
||||
expect(result).toBe('First Value, Second Value');
|
||||
});
|
||||
|
||||
it('should filter out empty values from array', () => {
|
||||
const property = ['Value 1', '', 'Value 3', { val: '' }] as any;
|
||||
|
||||
const result = icalDataExtractPropertyValue(property);
|
||||
|
||||
expect(result).toBe('Value 1, Value 3');
|
||||
});
|
||||
|
||||
it('should return default value when array contains only empty values', () => {
|
||||
const property = ['', { val: '' }, null] as any;
|
||||
|
||||
const result = icalDataExtractPropertyValue(property, 'No values');
|
||||
|
||||
expect(result).toBe('No values');
|
||||
});
|
||||
|
||||
it('should handle mixed array of strings and objects', () => {
|
||||
const property = [
|
||||
'Plain String',
|
||||
{ val: 'Object Value', params: {} },
|
||||
'Another String',
|
||||
] as any;
|
||||
|
||||
const result = icalDataExtractPropertyValue(property);
|
||||
|
||||
expect(result).toBe('Plain String, Object Value, Another String');
|
||||
});
|
||||
});
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { isNonEmptyString, isString } from '@sniptt/guards';
|
||||
import { isDefined } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Extracts the string value from an iCal property that may have parameters.
|
||||
* Per RFC 5545, properties can have parameters like LANGUAGE=de-DE, which causes
|
||||
* node-ical to return an object with `val` and `params` instead of a plain string.
|
||||
*
|
||||
* RFC 5545 Section 3.1.2 also allows multiple values in a single property.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2 (Property Parameters)
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.1.2 (Multiple Values)
|
||||
*/
|
||||
export const icalDataExtractPropertyValue = (
|
||||
property:
|
||||
| string
|
||||
| { val?: string; params?: Record<string, unknown> }
|
||||
| undefined,
|
||||
defaultValue = '',
|
||||
): string => {
|
||||
if (!isDefined(property)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (isNonEmptyString(property)) {
|
||||
return property;
|
||||
}
|
||||
|
||||
if (isDefined(property) && typeof property === 'object') {
|
||||
if ('val' in property && isDefined(property.val)) {
|
||||
return isString(property.val) ? property.val : String(property.val);
|
||||
}
|
||||
|
||||
if (Array.isArray(property)) {
|
||||
const values = property
|
||||
.map((item) => {
|
||||
if (isNonEmptyString(item)) return item;
|
||||
if (isDefined(item) && typeof item === 'object' && item?.val)
|
||||
return String(item.val);
|
||||
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return values.length > 0 ? values.join(', ') : defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
Reference in New Issue
Block a user