From ef2519db7eb114fbbf11a7fa75ca1c08384e79b4 Mon Sep 17 00:00:00 2001 From: Eduardo Conde Pena Date: Fri, 19 Jun 2020 08:05:01 +0000 Subject: [PATCH] Allow DTSTART <= DTEND in import and exclude EXDATE from recurrence rule consistency check CALWEB-1132 CALWEB-1130 --- src/app/components/import/ErrorDetails.tsx | 2 +- src/app/components/import/ImportEventError.ts | 4 - src/app/helpers/import.ts | 20 +++-- src/app/helpers/rrule.ts | 3 +- test/import/import.spec.ts | 74 ++++++++++++++++--- test/rrule/rrule.spec.js | 30 ++++++++ 6 files changed, 108 insertions(+), 25 deletions(-) diff --git a/src/app/components/import/ErrorDetails.tsx b/src/app/components/import/ErrorDetails.tsx index 3bb0954..54a9bf5 100644 --- a/src/app/components/import/ErrorDetails.tsx +++ b/src/app/components/import/ErrorDetails.tsx @@ -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`; diff --git a/src/app/components/import/ImportEventError.ts b/src/app/components/import/ImportEventError.ts index 9aaca05..8203423 100644 --- a/src/app/components/import/ImportEventError.ts +++ b/src/app/components/import/ImportEventError.ts @@ -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, @@ -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`; } diff --git a/src/app/helpers/import.ts b/src/app/helpers/import.ts index cb544b0..fd74b2b 100644 --- a/src/app/helpers/import.ts +++ b/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 { @@ -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'; @@ -287,7 +287,7 @@ 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, @@ -295,17 +295,21 @@ export const getSupportedEvent = ({ vcalComponent, hasXWrTimezone, calendarTzid 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); diff --git a/src/app/helpers/rrule.ts b/src/app/helpers/rrule.ts index a191426..1e14049 100644 --- a/src/app/helpers/rrule.ts +++ b/src/app/helpers/rrule.ts @@ -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, @@ -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; } diff --git a/test/import/import.spec.ts b/test/import/import.spec.ts index 0bba3ca..710146d 100644 --- a/test/import/import.spec.ts +++ b/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'; @@ -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', () => { @@ -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: [ { @@ -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'; @@ -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) }, diff --git a/test/rrule/rrule.spec.js b/test/rrule/rrule.spec.js index 916db66..2b02239 100644 --- a/test/rrule/rrule.spec.js +++ b/test/rrule/rrule.spec.js @@ -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); + }); });