Skip to content
This repository has been archived by the owner on Aug 11, 2021. It is now read-only.

Commit

Permalink
Allow DTSTART <= DTEND in import and exclude EXDATE from recurrence r…
Browse files Browse the repository at this point in the history
…ule consistency check

CALWEB-1132
CALWEB-1130
  • Loading branch information
econdepe authored and mmso committed Jun 19, 2020
1 parent 1c25533 commit ef2519d
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 25 deletions.
2 changes: 1 addition & 1 deletion src/app/components/import/ErrorDetails.tsx
Expand Up @@ -7,7 +7,7 @@ import { MAX_UID_CHARS_DISPLAY } from '../../constants';

const getComponentText = (component: string) => {
if (component === '') {
return c('Error importing event').t`Bad format. Component can not be read.`;
return c('Error importing event').t`Bad format. Component cannot be read.`;
}
if (component === 'vcalendar') {
return c('Error importing event').t`Calendar`;
Expand Down
4 changes: 0 additions & 4 deletions src/app/components/import/ImportEventError.ts
Expand Up @@ -19,7 +19,6 @@ export enum IMPORT_EVENT_TYPE {
VEVENT_DURATION,
X_WR_TIMEZONE_UNSUPPORTED,
TZID_UNSUPPORTED,
NEGATIVE_DURATION,
RRULE_INCONSISTENT,
RRULE_UNSUPPORTED,
NOTIFICATION_OUT_OF_BOUNDS,
Expand Down Expand Up @@ -80,9 +79,6 @@ const getErrorMessage = (errorType: IMPORT_EVENT_TYPE, externalError?: Error) =>
if (errorType === IMPORT_EVENT_TYPE.TZID_UNSUPPORTED) {
return c('Error importing event').t`Timezone not supported`;
}
if (errorType === IMPORT_EVENT_TYPE.NEGATIVE_DURATION) {
return c('Error importing event').t`Negative duration`;
}
if (errorType === IMPORT_EVENT_TYPE.RRULE_INCONSISTENT) {
return c('Error importing event').t`Recurring rule inconsistent`;
}
Expand Down
20 changes: 12 additions & 8 deletions src/app/helpers/import.ts
@@ -1,3 +1,4 @@
import { c } from 'ttag';
import { parseWithErrors } from 'proton-shared/lib/calendar/vcal';
import { getDateProperty, getDateTimeProperty, propertyToUTCDate } from 'proton-shared/lib/calendar/vcalConverter';
import {
Expand Down Expand Up @@ -28,7 +29,6 @@ import {
VcalVcalendar,
VcalVeventComponent,
} from 'proton-shared/lib/interfaces/calendar/VcalModel';
import { c } from 'ttag';

import { IMPORT_EVENT_TYPE, ImportEventError } from '../components/import/ImportEventError';
import { IMPORT_ERROR_TYPE, ImportFileError } from '../components/import/ImportFileError';
Expand Down Expand Up @@ -287,25 +287,29 @@ export const getSupportedEvent = ({ vcalComponent, hasXWrTimezone, calendarTzid
throw new ImportEventError(IMPORT_EVENT_TYPE.DTSTART_OUT_OF_BOUNDS, 'vevent', componentId);
}
if (dtend) {
validated.dtend = getSupportedDateOrDateTimeProperty({
const supportedDtend = getSupportedDateOrDateTimeProperty({
property: dtend,
component: 'vevent',
componentId,
hasXWrTimezone,
calendarTzid,
isRecurring,
});
if (!getIsWellFormedDateOrDateTime(validated.dtend)) {
if (!getIsWellFormedDateOrDateTime(supportedDtend)) {
throw new ImportEventError(IMPORT_EVENT_TYPE.DTEND_MALFORMED, 'vevent', componentId);
}
if (getIsDateOutOfBounds(validated.dtend)) {
if (getIsDateOutOfBounds(supportedDtend)) {
throw new ImportEventError(IMPORT_EVENT_TYPE.DTEND_OUT_OF_BOUNDS, 'vevent', componentId);
}
const startDateUTC = propertyToUTCDate(validated.dtstart);
const endDateUTC = propertyToUTCDate(validated.dtend);
const modifiedEndDateUTC = isAllDayEnd ? addDays(endDateUTC, -1) : endDateUTC;
if (+startDateUTC > +modifiedEndDateUTC) {
throw new ImportEventError(IMPORT_EVENT_TYPE.NEGATIVE_DURATION, 'vevent', componentId);
const endDateUTC = propertyToUTCDate(supportedDtend);
// allow a non-RFC-compliant all-day event with DTSTART = DTEND
const modifiedEndDateUTC =
!isAllDayEnd || +startDateUTC === +endDateUTC ? endDateUTC : addDays(endDateUTC, -1);
const duration = +modifiedEndDateUTC - +startDateUTC;

if (duration > 0) {
validated.dtend = supportedDtend;
}
} else if (duration) {
throw new ImportEventError(IMPORT_EVENT_TYPE.VEVENT_DURATION, 'vevent', componentId);
Expand Down
3 changes: 2 additions & 1 deletion src/app/helpers/rrule.ts
Expand Up @@ -2,6 +2,7 @@ import { getOccurrences } from 'proton-shared/lib/calendar/recurring';
import { propertyToUTCDate } from 'proton-shared/lib/calendar/vcalConverter';
import { getIsPropertyAllDay, getPropertyTzid } from 'proton-shared/lib/calendar/vcalHelper';
import { toLocalDate, toUTCDate } from 'proton-shared/lib/date/timezone';
import { omit } from 'proton-shared/lib/helpers/object';
import {
VcalDaysKeys,
VcalRruleProperty,
Expand Down Expand Up @@ -239,7 +240,7 @@ export const getHasConsistentRrule = (vevent: VcalVeventComponent) => {
}

// make sure DTSTART matches the pattern of the recurring series
const [first] = getOccurrences({ component: vevent, maxCount: 1 });
const [first] = getOccurrences({ component: omit(vevent, ['exdate']), maxCount: 1 });
if (!first) {
return false;
}
Expand Down
74 changes: 63 additions & 11 deletions test/import/import.spec.ts
@@ -1,6 +1,7 @@
import { parse } from 'proton-shared/lib/calendar/vcal';
import { truncate } from 'proton-shared/lib/helpers/string';
import { VcalVeventComponent } from 'proton-shared/lib/interfaces/calendar/VcalModel';
import { omit } from 'proton-shared/lib/helpers/object';
import { MAX_LENGTHS } from '../../src/app/constants';
import { getSupportedEvent } from '../../src/app/helpers/import';

Expand Down Expand Up @@ -61,18 +62,73 @@ END:VEVENT`;
);
});

test('should catch events with negative duration', () => {
test('should accept (and re-format) events with negative duration', () => {
const vevent = `BEGIN:VEVENT
DTSTAMP:19980309T231000Z
UID:test-event
DTSTART;TZID=America/New_York:20020312T083000
DTEND;TZID=America/New_York:20010312T083000
DTEND;TZID=America/New_York:20020312T082959
LOCATION:1CP Conference Room 4350
END:VEVENT`;
const event = parse(vevent) as VcalVeventComponent;
expect(() => getSupportedEvent({ vcalComponent: event, hasXWrTimezone: false })).toThrowError(
'Negative duration'
);
expect(getSupportedEvent({ vcalComponent: event, hasXWrTimezone: false })).toEqual({
component: 'vevent',
uid: { value: 'test-event' },
dtstamp: {
value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
},
dtstart: {
value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
parameters: { tzid: 'America/New_York' },
},
location: { value: '1CP Conference Room 4350' },
});
});

test('should drop DTEND for part-day events with zero duration', () => {
const vevent = `BEGIN:VEVENT
DTSTAMP:19980309T231000Z
UID:test-event
DTSTART;TZID=America/New_York:20020312T083000
DTEND;TZID=America/New_York:20020312T083000
LOCATION:1CP Conference Room 4350
END:VEVENT`;
const event = parse(vevent) as VcalVeventComponent;
expect(getSupportedEvent({ vcalComponent: event, hasXWrTimezone: false })).toEqual({
component: 'vevent',
uid: { value: 'test-event' },
dtstamp: {
value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
},
dtstart: {
value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
parameters: { tzid: 'America/New_York' },
},
location: { value: '1CP Conference Room 4350' },
});
});

test('should drop DTEND for all-day events with zero duration', () => {
const vevent = `BEGIN:VEVENT
DTSTAMP:19980309T231000Z
UID:test-event
DTSTART;VALUE=DATE:20020312
DTEND;VALUE=DATE:20020312
LOCATION:1CP Conference Room 4350
END:VEVENT`;
const event = parse(vevent) as VcalVeventComponent;
expect(getSupportedEvent({ vcalComponent: event, hasXWrTimezone: false })).toEqual({
component: 'vevent',
uid: { value: 'test-event' },
dtstamp: {
value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
},
dtstart: {
value: { year: 2002, month: 3, day: 12 },
parameters: { type: 'date' },
},
location: { value: '1CP Conference Room 4350' },
});
});

test('should catch events whose duration is specified through the DURATION field', () => {
Expand Down Expand Up @@ -143,10 +199,6 @@ END:VEVENT`;
value: { year: 1999, month: 3, day: 12 },
parameters: { type: 'date' },
},
dtend: {
value: { year: 1999, month: 3, day: 13 },
parameters: { type: 'date' },
},
location: { value: '1CP Conference Room 4350' },
components: [
{
Expand Down Expand Up @@ -356,7 +408,7 @@ BEGIN:VEVENT
DTSTAMP:19980309T231000Z
UID:test-event
DTSTART;VALUE=DATE:20200518
DTEND;VALUE=DATE:20200519
DTEND;VALUE=DATE:20200520
LOCATION:1CP Conference Room 4350
END:VEVENT`;
const tzid = 'Europe/Brussels';
Expand Down Expand Up @@ -396,7 +448,7 @@ END:VEVENT`;
const event = parse(vevent) as VcalVeventComponent;
expect(croppedUID.length === MAX_LENGTHS.UID);
expect(getSupportedEvent({ vcalComponent: event, hasXWrTimezone: false })).toEqual({
...event,
...omit(event, ['dtend']),
uid: { value: croppedUID },
summary: { value: truncate(loremIpsum, MAX_LENGTHS.TITLE) },
location: { value: truncate(loremIpsum, MAX_LENGTHS.LOCATION) },
Expand Down
30 changes: 30 additions & 0 deletions test/rrule/rrule.spec.js
Expand Up @@ -337,4 +337,34 @@ describe('getHasConsistentRrule', () => {
const expected = vevents.map(() => false);
expect(vevents.map((vevent) => getHasConsistentRrule(vevent))).toEqual(expected);
});

test('should exclude exdate when checking consistency', () => {
const vevent = {
dtstart: {
value: { year: 2015, month: 8, day: 25, hours: 18, minutes: 30, seconds: 0, isUTC: false },
parameters: {
tzid: 'Europe/Paris',
},
},
dtend: {
value: { year: 2015, month: 8, day: 25, hours: 18, minutes: 35, seconds: 0, isUTC: false },
parameters: {
tzid: 'Europe/Paris',
},
},
rrule: {
value: {
freq: 'DAILY',
until: { year: 2015, month: 8, day: 26, hours: 16, minutes: 29, seconds: 59, isUTC: true },
},
},
exdate: {
value: { year: 2015, month: 8, day: 25, hours: 18, minutes: 30, seconds: 0, isUTC: false },
parameters: {
tzid: 'Europe/Paris',
},
},
};
expect(getHasConsistentRrule(vevent)).toEqual(true);
});
});

0 comments on commit ef2519d

Please sign in to comment.