fugitsit
Version:
Small library that wraps ICAL.js and provide more convenient means for editing
599 lines (525 loc) • 15.6 kB
JavaScript
/**
* @copyright Copyright (c) 2019 Georg Ehrke
*
* @author Georg Ehrke <georg-nextcloud@ehrke.email>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import AbstractParser from './abstractParser.js'
import CalendarComponent from '../components/calendarComponent.js'
import { getRepairSteps } from './repairsteps/icalendar'
import { uc } from '../helpers/stringHelper.js'
import { getConstructorForComponentName } from '../components/root'
import { getTimezoneManager } from '../timezones/timezoneManager.js'
import Timezone from '../timezones/timezone.js'
import RecurrenceManager from '../recurrence/recurrenceManager.js'
import DateTimeValue from '../values/dateTimeValue.js'
import ICAL from 'ical.js'
/**
* @class ICalendarParser
* @classdesc
*/
export default class ICalendarParser extends AbstractParser {
/**
* @inheritDoc
*/
constructor(...args) {
super(...args)
/**
* The raw text/calendar data
*
* @type {String}
* @protected
*/
this._rawData = null
/**
* The CalendarComponent representing the raw data
*
* @type {CalendarComponent}
* @protected
*/
this._calendarComponent = null
/**
* A flag whether this calendar-data contains vevents
*
* @type {boolean}
* @private
*/
this._containsVEvents = false
/**
* A flag whether this calendar-data contains vjournals
*
* @type {boolean}
* @private
*/
this._containsVJournals = false
/**
* A flag whether this calendar-data contains vtodos
*
* @type {boolean}
* @private
*/
this._containsVTodos = false
/**
* A flag whether this calendar-data contains vfreebusy
*
* @type {boolean}
* @private
*/
this._containsVFreeBusy = false
/**
* A map containing all VObjects.
* The key of this map is the UID
* The value an array of all VObjects with that particular UID
*
* @type {Map<String, AbstractRecurringComponent[]>}
* @private
*/
this._items = new Map()
/**
* Items that are no recurrence-exceptions
* The key of this map is the UID
*
* @type {Map<String, AbstractRecurringComponent>}
* @private
*/
this._masterItems = new Map()
/**
* Items that are recurrence exceptions
*
* @type {Map<String, AbstractRecurringComponent[]>}
* @private
*/
this._recurrenceExceptionItems = new Map()
/**
* Some recurrence-exceptions come without a master item
* In that case we need to forge a master item
*
* @type {Map<String, AbstractRecurringComponent>}
* @private
*/
this._forgedMasterItems = new Map()
/**
* A list of timezone-components found in the calendar-data
*
* @type {Map<String, Timezone>}
* @private
*/
this._timezones = new Map()
/**
* A set of required timezones for each UID
*
* @type {Map<String, Set<String>>}
* @private
*/
this._requiredTimezones = new Map()
/**
* Instance of the default timezone-manager
*
* @type {TimezoneManager}
* @private
*/
this._defaultTimezoneManager = getTimezoneManager()
}
/**
* Parses the actual calendar-data
*
* @param {String} ics The icalendar data to parse
*/
parse(ics) {
this._rawData = ics
this._applyRepairSteps()
// If a timezone is not inside our TimezoneManager at the time of parsing
// the internal zone will be marked as floating inside ICAL.Time
// so before we start any actual parsing, we need to extract all timezones
// and add them to the TimezoneManager
this._extractTimezones()
this._registerTimezones()
this._createCalendarComponent()
if (this._getOption('extractGlobalProperties', false)) {
this._extractProperties()
}
this._processVObjects()
if (this._getOption('processFreeBusy', false)) {
this._processVFreeBusy()
}
}
/**
* @inheritDoc
*/
* getItemIterator() {
for (const itemList of this._items.values()) {
const calendarComp = CalendarComponent.fromEmpty()
if (this._getOption('includeTimezones', false)) {
this._addRequiredTimezonesToCalendarComp(calendarComp, itemList[0].uid)
}
// Preserve the original product id, just in case we need special handling for certain clients later on ...
if (this._calendarComponent.hasProperty('PRODID')) {
calendarComp.deleteAllProperties('PRODID')
calendarComp.addProperty(this._calendarComponent.getFirstProperty('PRODID').clone())
}
if (this._getOption('preserveMethod', false)) {
if (this._calendarComponent.hasProperty('METHOD')) {
calendarComp.deleteAllProperties('METHOD')
calendarComp.addProperty(this._calendarComponent.getFirstProperty('METHOD').clone())
}
}
for (const item of itemList) {
calendarComp.addComponent(item)
}
yield calendarComp
}
}
/**
* @inheritDoc
*/
containsVEvents() {
return this._containsVEvents
}
/**
* @inheritDoc
*/
containsVJournals() {
return this._containsVJournals
}
/**
* @inheritDoc
*/
containsVTodos() {
return this._containsVTodos
}
/**
* @inheritDoc
*/
containsVFreeBusy() {
return this._containsVFreeBusy
}
/**
* @inheritDoc
*/
getItemCount() {
return Array.from(this._items.keys()).length
}
/**
* Applies all registered repair steps
*
* @private
*/
_applyRepairSteps() {
for (const RepairStep of getRepairSteps()) {
const step = new RepairStep()
this._rawData = step.repair(this._rawData)
}
}
/**
* Creates a calendar component based upon the repaired data
*
* @private
*/
_createCalendarComponent() {
const jCal = ICAL.parse(this._rawData)
const icalComp = new ICAL.Component(jCal)
this._calendarComponent = CalendarComponent.fromICALJs(icalComp)
}
/**
* extracts properties
*
* @protected
*/
_extractProperties() {
this._extractPropertyAndPutResultIntoVariable(['name', 'x-wr-calname'], '_name')
this._extractPropertyAndPutResultIntoVariable(['color', 'x-apple-calendar-color'], '_color')
this._extractPropertyAndPutResultIntoVariable(['source'], '_sourceURL')
this._extractPropertyAndPutResultIntoVariable(['refresh-interval', 'x-published-ttl'], '_refreshInterval')
this._extractPropertyAndPutResultIntoVariable(['x-wr-timezone'], '_calendarTimezone')
}
/**
* Extract a property and writes it into a class property
* names must be an array, it will use the value of the fist
* propertyname it can find
*
* @param {String[]} names The names of the properties to check
* @param {String} variableName The variable name to save it under
* @private
*/
_extractPropertyAndPutResultIntoVariable(names, variableName) {
for (const name of names) {
if (this._calendarComponent.hasProperty(name)) {
this[variableName] = this._calendarComponent.getFirstPropertyFirstValue(name)
return
}
}
}
/**
* Extracts timezones from the calendar component
*
* @protected
*/
_extractTimezones() {
const matches = this._rawData.match(/^BEGIN:VTIMEZONE$(((?!^END:VTIMEZONE$)(.|\n))*)^END:VTIMEZONE$\n/gm)
if (!matches) {
return
}
for (const match of matches) {
const tzidMatcher = match.match(/^TZID:(.*)$/gm)
if (!tzidMatcher) {
continue
}
const tzid = tzidMatcher[0].substr(5)
const timezone = new Timezone(tzid, match)
this._timezones.set(tzid, timezone)
}
}
/**
* Registers unknown timezones into our timezone-manager
*
* @protected
*/
_registerTimezones() {
for (const [tzid, timezone] of this._timezones) {
if (!this._defaultTimezoneManager.hasTimezoneForId(tzid)) {
this._defaultTimezoneManager.registerTimezone(timezone)
}
}
}
/**
* Processes the parsed vobjects
*
* @protected
*/
_processVObjects() {
for (const vObject of this._calendarComponent.getVObjectIterator()) {
this._addItem(vObject)
this._markCompTypeAsSeen(vObject.name)
if (vObject.isRecurrenceException()) {
this._addRecurrenceException(vObject)
} else {
vObject.recurrenceManager = new RecurrenceManager(vObject)
this._masterItems.set(vObject.uid, vObject)
}
for (const propertyToCheck of vObject.getPropertyIterator()) {
for (const value of propertyToCheck.getValueIterator()) {
if (value instanceof DateTimeValue && value.timezoneId) {
this._addRequiredTimezone(vObject.uid, value.timezoneId)
}
}
}
// TRIGGER is supposed to be stored in UTC only,
// but not all clients stick to this
for (const alarm of vObject.getAlarmIterator()) {
for (const propertyToCheck of alarm.getPropertyIterator()) {
for (const value of propertyToCheck.getValueIterator()) {
if (value instanceof DateTimeValue && value.timezoneId) {
this._addRequiredTimezone(vObject.uid, value.timezoneId)
}
}
}
}
if (this._getOption('removeRSVPForAttendees', false)) {
for (const attendee of vObject.getAttendeeIterator()) {
attendee.deleteParameter('RSVP')
}
}
}
for (const recurrenceExceptionList of this._recurrenceExceptionItems.values()) {
for (const recurrenceException of recurrenceExceptionList) {
// Check if there is a master item for this recurrence exception
// otherwise we have to forge one
if (!this._masterItems.has(recurrenceException.uid)) {
const constructor = getConstructorForComponentName(recurrenceException.name)
const forgedMaster = new constructor(recurrenceException.name, [
['UID', recurrenceException.uid],
['DTSTAMP', recurrenceException.stampTime.clone()],
['DTSTART', recurrenceException.recurrenceId.clone()],
])
forgedMaster.recurrenceManager = new RecurrenceManager(forgedMaster)
this._forgedMasterItems.set(recurrenceException.uid, forgedMaster)
this._masterItems.set(recurrenceException.uid, forgedMaster)
this._addItem(forgedMaster)
} else {
const master = this._masterItems.get(recurrenceException.uid)
// This should usually not be the case,
// only if the calendar-data is seriously broken.
// Let's try to handle it anyway by adding it to
// forgedMasterItems, we will simply add RDATEs
// in the next step to make it recur
if (!master.isRecurring()) {
this._forgedMasterItems.set(master.uid, master)
}
}
if (this._forgedMasterItems.has(recurrenceException.uid)) {
const forgedMaster = this._forgedMasterItems.get(recurrenceException.uid)
forgedMaster.recurrenceManager.addRecurrenceDate(false, recurrenceException.recurrenceId.clone())
}
const masterItem = this._masterItems.get(recurrenceException.uid)
masterItem.recurrenceManager.relateRecurrenceException(recurrenceException)
}
}
}
/**
* Process FreeBusy components
*
* @private
*/
_processVFreeBusy() {
for (const vObject of this._calendarComponent.getFreebusyIterator()) {
this._addItem(vObject)
this._markCompTypeAsSeen(vObject.name)
for (const propertyToCheck of vObject.getPropertyIterator()) {
for (const value of propertyToCheck.getValueIterator()) {
if (value instanceof DateTimeValue && value.timezoneId) {
this._addRequiredTimezone(vObject.uid, value.timezoneId)
}
}
}
}
}
/**
*
* @param {AbstractRecurringComponent} item The recurrence-item to register
* @private
*/
_addRecurrenceException(item) {
if (this._recurrenceExceptionItems.has(item.uid)) {
const arr = this._recurrenceExceptionItems.get(item.uid)
arr.push(item)
} else {
this._recurrenceExceptionItems.set(item.uid, [item])
}
}
/**
*
* @param {AbstractRecurringComponent} item The item to register
* @private
*/
_addItem(item) {
if (this._items.has(item.uid)) {
const arr = this._items.get(item.uid)
arr.push(item)
} else {
this._items.set(item.uid, [item])
}
}
/**
*
* @param {String} uid The uid of the calendar-object
* @param {String} timezoneId The timezoneId required by the object
* @private
*/
_addRequiredTimezone(uid, timezoneId) {
if (timezoneId === 'UTC' || timezoneId === 'floating' || timezoneId === 'GMT' || timezoneId === 'Z') {
return
}
if (this._requiredTimezones.has(uid)) {
this._requiredTimezones.get(uid).add(timezoneId)
} else {
const set = new Set([timezoneId])
this._requiredTimezones.set(uid, set)
}
}
/**
*
* @param {CalendarComponent} calendarComp The calendar-component to add timezones to
* @param {String} uid The UID of the calendar-object
* @private
*/
_addRequiredTimezonesToCalendarComp(calendarComp, uid) {
if (!this._requiredTimezones.has(uid)) {
return
}
for (const requiredTimezone of this._requiredTimezones.get(uid)) {
if (!this._defaultTimezoneManager.hasTimezoneForId(requiredTimezone)) {
return
}
const timezone = this._defaultTimezoneManager.getTimezoneForId(requiredTimezone)
// Is this an alias?
if (timezone.timezoneId !== requiredTimezone) {
this._replaceTimezoneWithAnotherOne(calendarComp, requiredTimezone, timezone.timezoneId)
}
const timezoneComponent = timezone.toTimezoneComponent()
calendarComp.addComponent(timezoneComponent)
}
}
/**
* Replaces all occurrences of searchTimezone with replaceTimezone
*
* @param {CalendarComponent} calendarComponent The calendar-component to replace a timezone in
* @param {String} searchTimezone The timezone to replace
* @param {String} replaceTimezone The replacement timezone
* @private
*/
_replaceTimezoneWithAnotherOne(calendarComponent, searchTimezone, replaceTimezone) {
for (const vObject of this._calendarComponent.getVObjectIterator()) {
for (const propertyToCheck of vObject.getPropertyIterator()) {
for (const value of propertyToCheck.getValueIterator()) {
if (!(value instanceof DateTimeValue)) {
continue
}
if (value.timezoneId === searchTimezone) {
value.silentlyReplaceTimezone(replaceTimezone)
}
}
}
// TRIGGER is supposed to be stored in UTC only,
// but not all clients stick to this
for (const alarm of vObject.getAlarmIterator()) {
for (const propertyToCheck of alarm.getPropertyIterator()) {
for (const value of propertyToCheck.getValueIterator()) {
if (!(value instanceof DateTimeValue)) {
continue
}
if (value.timezoneId === searchTimezone) {
value.silentlyReplaceTimezone(replaceTimezone)
}
}
}
}
}
}
/**
* Marks a certain component type as seen.
* This is used for
* containsVEvents()
* containsVJournals()
* containsVTodos()
*
* @param {String} compName The name of the visited component
* @private
*/
_markCompTypeAsSeen(compName) {
switch (uc(compName)) {
case 'VEVENT':
this._containsVEvents = true
break
case 'VJOURNAL':
this._containsVJournals = true
break
case 'VTODO':
this._containsVTodos = true
break
case 'VFREEBUSY':
this._containsVFreeBusy = true
break
}
}
/**
* @inheritDoc
*/
static getMimeTypes() {
return ['text/calendar']
}
}