Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/models/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ import {
mapRecurrenceRuleValueToRecurrenceRuleObject,
} from './recurrenceRule.js'

/**
* Removes empty values from BYMONTHDAY lists before parsing. Some CalDAV
* clients send values such as `BYMONTHDAY=,24`, which ical.js rejects.
* @param {string} vcalendar The vCalendar data
* @return {string}
*/
function normalizeEmptyByMonthDayValues(vcalendar) {
return vcalendar.replace(/(RRULE(?:;[^:\r\n]*)?:)([^\r\n]*)/gi, (line, prefix, rule) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize folded RRULE content lines

When a CalDAV payload uses standard line folding before or within BYMONTHDAY (for example, RRULE:FREQ=MONTHLY;\r\n BYMONTHDAY=,24), this pattern stops at the physical CR/LF and never examines the continuation. ICAL.parse then unfolds the unchanged malformed value and rejects it, so the task/calendar disappearance this change is meant to prevent remains for folded recurrence rules; unfold the content line or include its continuations before sanitizing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Anchor normalization to RRULE property lines

Whenever another property contains the substring RRULE:—for example the valid URI URL:https://example.test/inspect?RRULE:FREQ=MONTHLY;BYMONTHDAY=,24—this unanchored regexp treats the rest of that property as a recurrence rule and removes the comma from its value. The constructor stores the changed value in jCal, which updateTask later serializes via ICAL.stringify(task.jCal) in src/store/tasks.js:1114, silently corrupting URL or extension data on the next edit; restrict matching to an RRULE property name at the start of an unfolded content line.

Useful? React with 👍 / 👎.

return prefix + rule.replace(/(^|;)BYMONTHDAY=([^;]*)/i, (part, separator, days) => {
const normalizedDays = days.split(',').filter(Boolean).join(',')
return normalizedDays ? `${separator}BYMONTHDAY=${normalizedDays}` : ''

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drop the following delimiter with a leading empty part

When BYMONTHDAY is the first rule part and every entry is empty, for example RRULE:BYMONTHDAY=,;FREQ=MONTHLY, separator is empty and this branch emits RRULE:;FREQ=MONTHLY. Recurrence parts may legally precede FREQ, but the resulting leading empty part is invalid and ical.js rejects it, so these tasks still fail to load instead of being normalized; when dropping the first part, consume the following semicolon as well.

Useful? React with 👍 / 👎.

})
})
}

export default class Task {

/**
Expand All @@ -48,7 +63,7 @@ export default class Task {
throw new Error('Invalid vCalendar')
}

const jCal = ICAL.parse(vcalendar)
const jCal = ICAL.parse(normalizeEmptyByMonthDayValues(vcalendar))
if (jCal[0] !== 'vcalendar') {
throw new Error('Only one task is allowed in the vCalendar data')
}
Expand Down
17 changes: 17 additions & 0 deletions tests/javascript/unit/models/task.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ describe('task', () => {
expect(task.isRecurring).toEqual(true)
})

it('Should parse a BYMONTHDAY list with an empty value', () => {
const vcalendar = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VTODO
UID:empty-month-day
DTSTAMP:20261018T000000Z
DTSTART:20261001T020000Z
RRULE:FREQ=MONTHLY;BYMONTHDAY=,24;UNTIL=20261225T020000Z
SUMMARY:Repro
END:VTODO
END:VCALENDAR`
const task = new Task(vcalendar, {})

expect(task.isRecurring).toEqual(true)
expect(task.recurrenceRule.byMonthDay).toEqual([24])
})

it('Should set status to "COMPLETED" on completion.', () => {
const task = new Task(loadICS('vcalendars/vcalendar-default'), {})
task.complete = 100
Expand Down