Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / calendar / vcal.ts
blobe8f4028c9b1c7f6fbbad79af33088bcf01c8e18b
1 /**
2  * This file needs to be improved in terms of typing. They were rushed due to time constraints.
3  */
4 import ICAL from 'ical.js';
6 import { parseWithRecovery } from '@proton/shared/lib/calendar/icsSurgery/ics';
8 import { DAY, HOUR, MINUTE, SECOND, WEEK } from '../constants';
9 import type {
10     VcalCalendarComponent,
11     VcalCalendarComponentWithMaybeErrors,
12     VcalDateOrDateTimeValue,
13     VcalDateTimeValue,
14     VcalDateValue,
15     VcalDurationValue,
16     VcalErrorComponent,
17     VcalRrulePropertyValue,
18     VcalValarmComponent,
19     VcalVcalendar,
20     VcalVcalendarWithMaybeErrors,
21     VcalVeventComponent,
22     VcalVeventComponentWithMaybeErrors,
23 } from '../interfaces/calendar';
24 import { UNIQUE_PROPERTIES } from './vcalDefinition';
25 import { getIsVcalErrorComponent } from './vcalHelper';
27 const getIcalDateValue = (value: any, tzid: string | undefined, isDate: boolean) => {
28     const icalTimezone = value.isUTC ? ICAL.Timezone.utcTimezone : ICAL.Timezone.localTimezone;
29     const icalData = {
30         year: value.year,
31         month: value.month,
32         day: value.day,
33         hour: value.hours || 0,
34         minute: value.minutes || 0,
35         second: value.seconds || 0,
36         isDate,
37     };
38     return ICAL.Time.fromData(icalData, icalTimezone);
41 const getIcalPeriodValue = (value: any, tzid: string | undefined) => {
42     return ICAL.Period.fromData({
43         // periods must be of date-time
44         start: value.start ? getIcalDateValue(value.start, tzid, false) : undefined,
45         end: value.end ? getIcalDateValue(value.end, tzid, false) : undefined,
46         duration: value.duration ? ICAL.Duration.fromData(value.duration) : undefined,
47     });
50 const getIcalDurationValue = (value?: any) => {
51     return ICAL.Duration.fromData(value);
54 const getIcalUntilValue = (value?: any) => {
55     if (!value) {
56         return;
57     }
58     return getIcalDateValue(value, '', typeof value.hours === 'undefined');
61 export const internalValueToIcalValue = (type: string, value: any, { tzid }: { tzid?: string } = {}) => {
62     if (Array.isArray(value)) {
63         return value;
64     }
65     if (typeof value === 'string') {
66         return value;
67     }
68     if (type === 'date' || type === 'date-time') {
69         return getIcalDateValue(value, tzid, type === 'date');
70     }
71     if (type === 'duration') {
72         return getIcalDurationValue(value);
73     }
74     if (type === 'period') {
75         return getIcalPeriodValue(value, tzid);
76     }
77     if (type === 'recur') {
78         if (!value.until) {
79             return ICAL.Recur.fromData(value);
80         }
81         const until = getIcalUntilValue(value.until);
82         return ICAL.Recur.fromData({ ...value, until });
83     }
84     return value.toString();
87 const getInternalDateValue = (value: any): VcalDateValue => {
88     return {
89         year: value.year,
90         month: value.month,
91         day: value.day,
92     };
95 export const getInternalDateTimeValue = (value: any): VcalDateTimeValue => {
96     return {
97         ...getInternalDateValue(value),
98         hours: value.hour,
99         minutes: value.minute,
100         seconds: value.second,
101         isUTC: value.zone.tzid === 'UTC',
102     };
105 const getInternalDurationValue = (value: any): VcalDurationValue => {
106     return {
107         weeks: value.weeks,
108         days: value.days,
109         hours: value.hours,
110         minutes: value.minutes,
111         seconds: value.seconds,
112         isNegative: value.isNegative,
113     };
116 const getInternalUntil = (value?: any): VcalDateOrDateTimeValue | undefined => {
117     if (!value) {
118         return;
119     }
120     return value.icaltype === 'date' ? getInternalDateValue(value) : getInternalDateTimeValue(value);
123 const getInternalRecur = (value?: any): VcalRrulePropertyValue | undefined => {
124     if (!value) {
125         return;
126     }
127     const result = {
128         ...value.toJSON(),
129     };
130     // COUNT = 0 gets ignored in the above step
131     if (value.count === 0) {
132         result.count = 0;
133     }
134     const until = getInternalUntil(value.until);
135     if (until) {
136         result.until = until;
137     }
138     return result;
142  * Convert from ical.js format to an internal format
143  */
144 export const icalValueToInternalValue = (type: string, value: any) => {
145     if (Array.isArray(value)) {
146         return value;
147     }
148     if (typeof value === 'string' || type === 'integer') {
149         return value;
150     }
151     if (type === 'date') {
152         return getInternalDateValue(value);
153     }
154     if (type === 'date-time') {
155         return getInternalDateTimeValue(value);
156     }
157     if (type === 'duration') {
158         return getInternalDurationValue(value);
159     }
160     if (type === 'period') {
161         const result: any = {};
162         if (value.start) {
163             result.start = getInternalDateTimeValue(value.start);
164         }
165         if (value.end) {
166             result.end = getInternalDateTimeValue(value.end);
167         }
168         if (value.duration) {
169             result.duration = getInternalDurationValue(value.duration);
170         }
171         return result;
172     }
173     if (type === 'recur') {
174         return getInternalRecur(value);
175     }
176     return value.toString();
180  * Get an ical property.
181  */
182 const getProperty = (name: string, { value, parameters }: any) => {
183     const property = new ICAL.Property(name);
185     const { type: specificType, ...restParameters } = parameters || {};
187     if (specificType) {
188         property.resetType(specificType);
189     }
191     const type = specificType || property.type;
193     if (property.isMultiValue && Array.isArray(value)) {
194         property.setValues(value.map((val) => internalValueToIcalValue(type, val, restParameters)));
195     } else {
196         property.setValue(internalValueToIcalValue(type, value, restParameters));
197     }
199     Object.keys(restParameters).forEach((key) => {
200         property.setParameter(key, restParameters[key]);
201     });
203     return property;
206 const addInternalProperties = (component: any, properties: any) => {
207     Object.keys(properties).forEach((name) => {
208         const jsonProperty = properties[name];
210         if (Array.isArray(jsonProperty)) {
211             jsonProperty.forEach((property) => {
212                 component.addProperty(getProperty(name, property));
213             });
214             return;
215         }
217         component.addProperty(getProperty(name, jsonProperty));
218     });
219     return component;
222 const fromInternalComponent = (properties: any) => {
223     const { component: name, components, ...restProperties } = properties;
225     const component = addInternalProperties(new ICAL.Component(name), restProperties);
227     if (Array.isArray(components)) {
228         components.forEach((otherComponent) => {
229             component.addSubcomponent(fromInternalComponent(otherComponent));
230         });
231     }
233     return component;
236 export const serialize = (component: any) => {
237     return fromInternalComponent(component).toString();
240 const getParameters = (type: string, property: any) => {
241     const allParameters = property.toJSON() || [];
242     const parameters = allParameters[1];
243     const isDefaultType = type === property.getDefaultType();
245     const result = {
246         ...parameters,
247     };
249     if (!isDefaultType) {
250         result.type = type;
251     }
253     return result;
256 const checkIfDateOrDateTimeValid = (dateOrDateTimeString: string, isDateType = false) => {
257     if (/--/.test(dateOrDateTimeString)) {
258         // just to be consistent with error messages from ical.js
259         const message = isDateType ? 'could not extract integer from' : 'invalid date-time value';
260         throw new Error(message);
261     }
264 const fromIcalProperties = (properties = []) => {
265     if (properties.length === 0) {
266         return;
267     }
268     return properties.reduce<{ [key: string]: any }>((acc, property: any) => {
269         const { name } = property;
271         if (!name) {
272             return acc;
273         }
274         const { type } = property;
275         if (['date-time', 'date'].includes(type)) {
276             checkIfDateOrDateTimeValid(property.toJSON()[3], type === 'date');
277         }
278         const values = property.getValues().map((value: any) => icalValueToInternalValue(type, value));
280         const parameters = getParameters(type, property);
281         const propertyAsObject = {
282             value: property.isMultiValue ? values : values[0],
283             ...(Object.keys(parameters).length && { parameters }),
284         };
286         if (UNIQUE_PROPERTIES.has(name)) {
287             acc[name] = propertyAsObject;
288             return acc;
289         }
291         if (!acc[name]) {
292             acc[name] = [];
293         }
295         // Exdate can be both an array and multivalue, force it to only be an array
296         if (name === 'exdate') {
297             const normalizedValues = values.map((value: any) => ({ ...propertyAsObject, value }));
299             acc[name] = acc[name].concat(normalizedValues);
300         } else {
301             acc[name].push(propertyAsObject);
302         }
304         return acc;
305     }, {});
308 export const fromIcalComponent = (component: any) => {
309     const components = component.getAllSubcomponents().map(fromIcalComponent);
310     return {
311         component: component.name,
312         ...(components.length && { components }),
313         ...fromIcalProperties(component ? component.getAllProperties() : undefined),
314     } as VcalCalendarComponent;
317 export const fromIcalComponentWithMaybeErrors = (
318     component: any
319 ): VcalCalendarComponentWithMaybeErrors | VcalErrorComponent => {
320     const components = component.getAllSubcomponents().map((subcomponent: any) => {
321         try {
322             return fromIcalComponentWithMaybeErrors(subcomponent);
323         } catch (error: any) {
324             return { error, icalComponent: subcomponent };
325         }
326     });
327     return {
328         component: component.name,
329         ...(components.length && { components }),
330         ...fromIcalProperties(component ? component.getAllProperties() : undefined),
331     } as VcalCalendarComponentWithMaybeErrors;
335  * Parse vCalendar String and return a component
336  */
337 export const parse = (vcal = ''): VcalCalendarComponent => {
338     if (!vcal) {
339         return {} as VcalCalendarComponent;
340     }
341     return fromIcalComponent(new ICAL.Component(ICAL.parse(vcal))) as VcalCalendarComponent;
345  * Same as the parseWithRecovery function, but catching errors in individual components.
346  * This is useful in case we can parse some events but not all in a given ics
347  */
348 export const parseVcalendarWithRecoveryAndMaybeErrors = (
349     vcal: string,
350     retry?: {
351         retryLineBreaks?: boolean;
352         retryEnclosing?: boolean;
353         retryDateTimes?: boolean;
354         retryOrganizer?: boolean;
355     }
356 ): VcalVcalendarWithMaybeErrors | VcalCalendarComponentWithMaybeErrors => {
357     try {
358         const result = parseWithRecovery(vcal, retry) as VcalVcalendar;
359         // add mandatory but maybe missing properties
360         if (!result.prodid) {
361             result.prodid = { value: '' };
362         }
363         if (!result.version) {
364             result.version = { value: '2.0' };
365         }
366         return result;
367     } catch (e) {
368         return fromIcalComponentWithMaybeErrors(new ICAL.Component(ICAL.parse(vcal))) as
369             | VcalVcalendarWithMaybeErrors
370             | VcalCalendarComponentWithMaybeErrors;
371     }
374 export const getVeventWithoutErrors = (
375     veventWithMaybeErrors: VcalVeventComponentWithMaybeErrors
376 ): VcalVeventComponent => {
377     const filteredComponents: VcalValarmComponent[] | undefined = veventWithMaybeErrors.components?.filter(
378         (component): component is VcalValarmComponent => !getIsVcalErrorComponent(component)
379     );
381     return {
382         ...veventWithMaybeErrors,
383         components: filteredComponents,
384     };
387 export const fromRruleString = (rrule = '') => {
388     return getInternalRecur(ICAL.Recur.fromString(rrule));
392  * Parse a trigger string (e.g. '-PT15M') and return an object indicating its duration
393  */
394 export const fromTriggerString = (trigger = '') => {
395     return getInternalDurationValue(ICAL.Duration.fromString(trigger));
398 export const toTriggerString = (value: VcalDurationValue): string => {
399     return getIcalDurationValue(value).toString();
403  * Transform a duration object into milliseconds
404  */
405 export const durationToMilliseconds = ({
406     isNegative = false,
407     weeks = 0,
408     days = 0,
409     hours = 0,
410     minutes = 0,
411     seconds = 0,
412     milliseconds = 0,
413 }) => {
414     const lapse = weeks * WEEK + days * DAY + hours * HOUR + minutes * MINUTE + seconds * SECOND + milliseconds;
415     return isNegative ? -lapse : lapse;
419  * Parse a trigger string (e.g. '-PT15M') and return its duration in milliseconds
420  */
421 export const getMillisecondsFromTriggerString = (trigger = '') => {
422     return durationToMilliseconds(fromTriggerString(trigger));