"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const ICAL = require("ical.js"); const uuid = require("uuid"); const timezones = require("@nextcloud/timezones"); const _interopDefault = (e) => e && e.__esModule ? e : { default: e }; const ICAL__default = /* @__PURE__ */ _interopDefault(ICAL); /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class AbstractParser { /** * @class * * @param {object=} options Object of options * @param {boolean=} options.extractGlobalProperties Whether or not to preserve properties from the VCALENDAR component (defaults to false) * @param {boolean=} options.removeRSVPForAttendees Whether or not to remove RSVP from attendees (defaults to false) * @param {boolean=} options.includeTimezones Whether or not to include timezones (defaults to false) * @param {boolean=} options.preserveMethod Whether or not to preserve the iCalendar method (defaults to false) * @param {boolean=} options.processFreeBusy Whether or not to process VFreeBusy components (defaults to false) */ constructor(options = {}) { if (new.target === AbstractParser) { throw new TypeError("Cannot instantiate abstract class AbstractParser"); } this._options = Object.assign({}, options); this._name = null; this._color = null; this._sourceURL = null; this._refreshInterval = null; this._calendarTimezone = null; this._errors = []; } /** * Gets the name extracted from the calendar-data * * @return {string | null} */ getName() { return this._name; } /** * Gets the color extracted from the calendar-data * * @return {string | null} */ getColor() { return this._color; } /** * Gets whether this import can be converted into a webcal subscription * * @return {boolean} */ offersWebcalFeed() { return this._sourceURL !== null; } /** * Gets the url pointing to the webcal source * * @return {string | null} */ getSourceURL() { return this._sourceURL; } /** * Gets the recommended refresh rate to update this subscription * * @return {string | null} */ getRefreshInterval() { return this._refreshInterval; } /** * Gets the default timezone of this calendar * * @return {string} */ getCalendarTimezone() { return this._calendarTimezone; } /** * {String|Object} data * * @param {any} data The data to parse * @throws TypeError */ parse(data) { throw new TypeError("Abstract method not implemented by subclass"); } /** * Returns one CalendarComponent at a time */ *getItemIterator() { throw new TypeError("Abstract method not implemented by subclass"); } /** * Get an array of all items * * @return {CalendarComponent[]} */ getAllItems() { return Array.from(this.getItemIterator()); } /** * Returns a boolean whether or not the parsed data contains vevents * * @return {boolean} */ containsVEvents() { return false; } /** * Returns a boolean whether or not the parsed data contains vjournals * * @return {boolean} */ containsVJournals() { return false; } /** * Returns a boolean whether or not the parsed data contains vtodos * * @return {boolean} */ containsVTodos() { return false; } /** * Returns a boolean whether or not the parsed data contains vfreebusys * * @return {boolean} */ containsVFreeBusy() { return false; } /** * Returns a boolean whether * * @return {boolean} */ hasErrors() { return this._errors.length !== 0; } /** * Get a list of all errors that occurred * * @return {*[]} */ getErrorList() { return this._errors.slice(); } /** * Returns the number of calendar-objects in parser * * @return {number} */ getItemCount() { return 0; } /** * Gets an option provided * * @param {string} name The name of the option to get * @param {*} defaultValue The default value to return if option not provided * @return {any} * @protected */ _getOption(name, defaultValue) { return Object.prototype.hasOwnProperty.call(this._options, name) ? this._options[name] : defaultValue; } /** * Return list of supported mime types * * @static */ static getMimeTypes() { throw new TypeError("Abstract method not implemented by subclass"); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ModificationNotAllowedError extends Error { } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ function lockableTrait(baseClass) { return class extends baseClass { /** * Constructor * * @param {...any} args */ constructor(...args) { super(...args); this._mutable = true; } /** * Returns whether or not this object is locked * * @return {boolean} */ isLocked() { return !this._mutable; } /** * Marks this object is immutable * locks it against further modification */ lock() { this._mutable = false; } /** * Marks this object as mutable * allowing further modification */ unlock() { this._mutable = true; } /** * Check if modifications are allowed * * @throws {ModificationNotAllowedError} if this object is locked for modification * @protected */ _modify() { if (!this._mutable) { throw new ModificationNotAllowedError(); } } /** * Check if modification of content is allowed * * @throws {ModificationNotAllowedError} if this object is locked for modification * @protected */ _modifyContent() { this._modify(); } }; } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ExpectedICalJSError extends Error { } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ function lc(str) { return str.toLowerCase(); } function uc(str) { return str.toUpperCase(); } function ucFirst(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function startStringWith(str, startWith) { if (!str.startsWith(startWith)) { str = startWith + str; } return str; } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ const GLOBAL_CONFIG = /* @__PURE__ */ new Map(); function setConfig(key, value) { GLOBAL_CONFIG.set(key, value); } function getConfig(key, defaultValue) { return GLOBAL_CONFIG.get(key) || defaultValue; } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ function createComponent(componentName) { return new ICAL__default.default.Component(lc(componentName)); } function createProperty(propertyName) { return new ICAL__default.default.Property(lc(propertyName)); } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ function observerTrait(baseClass) { return class extends baseClass { /** * Constructor * * @param {...any} args */ constructor(...args) { super(...args); this._subscribers = []; } /** * Adds a new subscriber * * @param {Function} handler - Handler to be called when modification happens */ subscribe(handler) { this._subscribers.push(handler); } /** * Removes a subscriber * * @param {Function} handler - Handler to be no longer called when modification happens */ unsubscribe(handler) { const index = this._subscribers.indexOf(handler); if (index === -1) { return; } this._subscribers.splice(index, 1); } /** * Notify all subscribed handlers * * @param {...any} args * @protected */ _notifySubscribers(...args) { for (const handler of this._subscribers) { handler(...args); } } }; } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class Parameter extends observerTrait(lockableTrait(class { })) { /** * Constructor * * @param {string} name The name of the parameter * @param {string|Array|null} value The value of the parameter */ constructor(name, value = null) { super(); this._name = uc(name); this._value = value; } /** * Get parameter name * * @readonly * @return {string} */ get name() { return this._name; } /** * Get parameter value * * @return {string | Array} */ get value() { return this._value; } /** * Set new parameter value * * @throws {ModificationNotAllowedError} if parameter is locked for modification * @param {string | Array} value The new value to set */ set value(value) { this._modifyContent(); this._value = value; } /** * Gets the first value of this parameter * * @return {string | null} */ getFirstValue() { if (!this.isMultiValue()) { return this.value; } else { if (this.value.length > 0) { return this.value[0]; } } return null; } /** * Gets an iterator for all values */ *getValueIterator() { if (this.isMultiValue()) { yield* this.value.slice()[Symbol.iterator](); } else { yield this.value; } } /** * Returns whether or not the value is a multivalue * * @return {boolean} */ isMultiValue() { return Array.isArray(this._value); } /** * Creates a copy of this parameter * * @return {Parameter} */ clone() { const parameter = new this.constructor(this._name); if (this.isMultiValue()) { parameter.value = this._value.slice(); } else { parameter.value = this._value; } return parameter; } /** * @inheritDoc */ _modifyContent() { super._modifyContent(); this._notifySubscribers(); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class AbstractValue extends observerTrait(lockableTrait(class { })) { /** * Constructor * * @param {ICAL.Binary|ICAL.Duration|ICAL.Period|ICAL.Recur|ICAL.Time|ICAL.UtcOffset} icalValue The ICAL.JS object to wrap */ constructor(icalValue) { if (new.target === AbstractValue) { throw new TypeError("Cannot instantiate abstract class AbstractValue"); } super(); this._innerValue = icalValue; } /** * Gets wrapped ICAL.JS object * * @return {*} */ toICALJs() { return this._innerValue; } /** * @inheritDoc */ _modifyContent() { super._modifyContent(); this._notifySubscribers(); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class BinaryValue extends AbstractValue { /** * Sets the raw b64 encoded value * * @return {string} */ get rawValue() { return this._innerValue.value; } /** * Gets the raw b64 encoded value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {string} value - The new raw value */ set rawValue(value) { this._modifyContent(); this._innerValue.value = value; } /** * Gets the decoded value * * @return {string} */ get value() { return this._innerValue.decodeValue(); } /** * Sets the decoded Value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {string} decodedValue - The new encoded value */ set value(decodedValue) { this._modifyContent(); this._innerValue.setEncodedValue(decodedValue); } /** * clones this value * * @return {BinaryValue} */ clone() { return BinaryValue.fromRawValue(this._innerValue.value); } /** * Create a new BinaryValue object from an ICAL.Binary object * * @param {ICAL.Binary} icalValue - The ICAL.Binary object * @return {BinaryValue} */ static fromICALJs(icalValue) { return new BinaryValue(icalValue); } /** * Create a new BinaryValue object from a raw b64 encoded value * * @param {string} rawValue - The raw value * @return {BinaryValue} */ static fromRawValue(rawValue) { const icalBinary = new ICAL__default.default.Binary(rawValue); return BinaryValue.fromICALJs(icalBinary); } /** * Create a new BinaryValue object from decoded value * * @param {string} decodedValue - The encoded value * @return {BinaryValue} */ static fromDecodedValue(decodedValue) { const icalBinary = new ICAL__default.default.Binary(); icalBinary.setEncodedValue(decodedValue); return BinaryValue.fromICALJs(icalBinary); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class DurationValue extends AbstractValue { /** * Gets the weeks of the stored duration-value * * @return {number} */ get weeks() { return this._innerValue.weeks; } /** * Sets the weeks of the stored duration-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if value is negative * @param {number} weeks Amount of weeks */ set weeks(weeks) { this._modifyContent(); if (weeks < 0) { throw new TypeError("Weeks cannot be negative, use isNegative instead"); } this._innerValue.weeks = weeks; } /** * Gets the days of the stored duration-value * * @return {number} */ get days() { return this._innerValue.days; } /** * Sets the days of the stored duration-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if value is negative * @param {number} days Amount of days */ set days(days) { this._modifyContent(); if (days < 0) { throw new TypeError("Days cannot be negative, use isNegative instead"); } this._innerValue.days = days; } /** * Gets the hours of the stored duration-value * * @return {number} */ get hours() { return this._innerValue.hours; } /** * Sets the weeks of the stored duration-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if value is negative * @param {number} hours Amount of hours */ set hours(hours) { this._modifyContent(); if (hours < 0) { throw new TypeError("Hours cannot be negative, use isNegative instead"); } this._innerValue.hours = hours; } /** * Gets the minutes of the stored duration-value * * @return {number} */ get minutes() { return this._innerValue.minutes; } /** * Sets the minutes of the stored duration-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if value is negative * @param {number} minutes Amount of minutes */ set minutes(minutes) { this._modifyContent(); if (minutes < 0) { throw new TypeError("Minutes cannot be negative, use isNegative instead"); } this._innerValue.minutes = minutes; } /** * Gets the seconds of the stored duration-value * * @return {number} */ get seconds() { return this._innerValue.seconds; } /** * Sets the seconds of the stored duration-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if value is negative * @param {number} seconds Amount of seconds */ set seconds(seconds) { this._modifyContent(); if (seconds < 0) { throw new TypeError("Seconds cannot be negative, use isNegative instead"); } this._innerValue.seconds = seconds; } /** * Gets the negative-indicator of the stored duration-value * * @return {boolean} */ get isNegative() { return this._innerValue.isNegative; } /** * Gets the negative-indicator of the stored duration-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {boolean} isNegative Whether or not the duration is negative */ set isNegative(isNegative) { this._modifyContent(); this._innerValue.isNegative = !!isNegative; } /** * Gets the amount of total seconds of the stored duration-value * * @return {* | number} */ get totalSeconds() { return this._innerValue.toSeconds(); } /** * Sets the amount of total seconds of the stored duration-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {number} totalSeconds The total amounts of seconds to set */ set totalSeconds(totalSeconds) { this._modifyContent(); this._innerValue.fromSeconds(totalSeconds); } /** * Compares this duration to another one * * @param {DurationValue} otherDuration The duration to compare to * @return {number} -1, 0 or 1 for less/equal/greater */ compare(otherDuration) { return this._innerValue.compare(otherDuration.toICALJs()); } /** * Adds the value of another duration to this one * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {DurationValue} otherDuration The duration to add */ addDuration(otherDuration) { this._modifyContent(); this.totalSeconds += otherDuration.totalSeconds; this._innerValue.normalize(); } /** * Subtract the value of another duration from this one * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {DurationValue} otherDuration The duration to subtract */ subtractDuration(otherDuration) { this._modifyContent(); this.totalSeconds -= otherDuration.totalSeconds; this._innerValue.normalize(); } /** * clones this value * * @return {DurationValue} */ clone() { return DurationValue.fromICALJs(this._innerValue.clone()); } /** * Create a new DurationValue object from an ICAL.Duration object * * @param {ICAL.Duration} icalValue The ical.js duration value * @return {DurationValue} */ static fromICALJs(icalValue) { return new DurationValue(icalValue); } /** * Create a new DurationValue object from a number of seconds * * @param {number} seconds Total amount of seconds * @return {DurationValue} */ static fromSeconds(seconds) { const icalDuration = ICAL__default.default.Duration.fromSeconds(seconds); return new DurationValue(icalDuration); } /** * Create a new DurationValue object from data * * @param {object} data The destructuring object * @param {number=} data.weeks Number of weeks to set * @param {number=} data.days Number of days to set * @param {number=} data.hours Number of hours to set * @param {number=} data.minutes Number of minutes to set * @param {number=} data.seconds Number of seconds to set * @param {boolean=} data.isNegative Whether or not duration is negative * @return {DurationValue} */ static fromData(data) { const icalDuration = ICAL__default.default.Duration.fromData(data); return new DurationValue(icalDuration); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class DateTimeValue extends AbstractValue { /** * Gets the year of the stored date-time-value * * @return {number} */ get year() { return this._innerValue.year; } /** * Sets the year of the stored date-time-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {number} year Number of years to set */ set year(year) { this._modifyContent(); this._innerValue.year = year; } /** * Gets the month of the stored date-time-value * * @return {number} */ get month() { return this._innerValue.month; } /** * Sets the month of the stored date-time-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {number} month Number of months to set */ set month(month) { this._modifyContent(); if (month < 1 || month > 12) { throw new TypeError("Month out of range"); } this._innerValue.month = month; } /** * Gets the day of the stored date-time-value * * @return {number} */ get day() { return this._innerValue.day; } /** * Sets the day of the stored date-time-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if out of range * @param {number} day Number of days to set */ set day(day) { this._modifyContent(); if (day < 1 || day > 31) { throw new TypeError("Day out of range"); } this._innerValue.day = day; } /** * Gets the hour of the stored date-time-value * * @return {number} */ get hour() { return this._innerValue.hour; } /** * Sets the hour of the stored date-time-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if out of range * @param {number} hour Number of hours to set */ set hour(hour) { this._modifyContent(); if (hour < 0 || hour > 23) { throw new TypeError("Hour out of range"); } this._innerValue.hour = hour; } /** * Gets the minute of the stored date-time-value * * @return {number} */ get minute() { return this._innerValue.minute; } /** * Sets the minute of the stored date-time-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if out of range * @param {number} minute Number of minutes to set */ set minute(minute) { this._modifyContent(); if (minute < 0 || minute > 59) { throw new TypeError("Minute out of range"); } this._innerValue.minute = minute; } /** * Gets the second of the stored date-time-value * * @return {number} */ get second() { return this._innerValue.second; } /** * Sets the second of the stored date-time-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if out of range * @param {number} second Number of seconds to set */ set second(second) { this._modifyContent(); if (second < 0 || second > 59) { throw new TypeError("Second out of range"); } this._innerValue.second = second; } /** * Gets the timezone of this date-time-value * * @return {string | null} */ get timezoneId() { if (this._innerValue.zone.tzid && this._innerValue.zone.tzid !== "floating" && this._innerValue.zone.tzid === "UTC") { return this._innerValue.zone.tzid; } if (this._innerValue.timezone) { return this._innerValue.timezone; } return this._innerValue.zone.tzid || null; } /** * Gets whether this date-time-value is a date or date-time * * @return {boolean} */ get isDate() { return this._innerValue.isDate; } /** * Sets whether this date-time-value is a date or date-time * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {boolean} isDate Whether this is a date or date-time value */ set isDate(isDate) { this._modifyContent(); this._innerValue.isDate = !!isDate; if (isDate) { this._innerValue.hour = 0; this._innerValue.minute = 0; this._innerValue.second = 0; } } /** * Gets the unix-time * * @return {number} */ get unixTime() { return this._innerValue.toUnixTime(); } /** * returns vanilla javascript date object * * @return {Date} */ get jsDate() { return this._innerValue.toJSDate(); } /** * Adds a duration to this date-time-value * * @param {DurationValue} duration The duration to ad */ addDuration(duration) { this._innerValue.addDuration(duration.toICALJs()); } /** * Subtract another date excluding timezones * * @param {DateTimeValue} other The date-time value to subtract * @return {DurationValue} */ subtractDateWithoutTimezone(other) { const icalDuration = this._innerValue.subtractDate(other.toICALJs()); return DurationValue.fromICALJs(icalDuration); } /** * Subtract another date, taking timezones into account * * @param {DateTimeValue} other The date-time value to subtract * @return {DurationValue} */ subtractDateWithTimezone(other) { const icalDuration = this._innerValue.subtractDateTz(other.toICALJs()); return DurationValue.fromICALJs(icalDuration); } /** * Compares this DateTimeValue object with another one * * @param {DateTimeValue} other The date-time to compare to * @return {number} -1, 0 or 1 for less/equal/greater */ compare(other) { return this._innerValue.compare(other.toICALJs()); } /** * Compares only the date part in a given timezone * * @param {DateTimeValue} other The date-time to compare to * @param {Timezone} timezone The timezone to compare in * @return {number} -1, 0 or 1 for less/equal/greater */ compareDateOnlyInGivenTimezone(other, timezone) { return this._innerValue.compareDateOnlyTz(other.toICALJs(), timezone.toICALTimezone()); } /** * Returns a clone of this object which was converted to a different timezone * * @param {Timezone} timezone TimezoneId to convert to * @return {DateTimeValue} */ getInTimezone(timezone) { const clonedICALTime = this._innerValue.convertToZone(timezone.toICALTimezone()); return DateTimeValue.fromICALJs(clonedICALTime); } /** * Get the inner ICAL.Timezone * * @return {ICAL.Timezone} * @package */ getICALTimezone() { return this._innerValue.zone; } /** * Returns a clone of this object which was converted to a different timezone * * @param {ICAL.Timezone} timezone TimezoneId to convert to * @return {DateTimeValue} * @package */ getInICALTimezone(timezone) { const clonedICALTime = this._innerValue.convertToZone(timezone); return DateTimeValue.fromICALJs(clonedICALTime); } /** * Returns a clone of this object which was converted to UTC * * @return {DateTimeValue} */ getInUTC() { const clonedICALTime = this._innerValue.convertToZone(ICAL__default.default.Timezone.utcTimezone); return DateTimeValue.fromICALJs(clonedICALTime); } /** * This silently replaces the inner timezone without converting the actual time * * @param {ICAL.Timezone} timezone The timezone to replace with * @package */ silentlyReplaceTimezone(timezone) { this._modify(); this._innerValue = new ICAL__default.default.Time({ year: this.year, month: this.month, day: this.day, hour: this.hour, minute: this.minute, second: this.second, isDate: this.isDate, timezone }); } /** * Replaces the inner timezone without converting the actual time * * @param {Timezone} timezone The timezone to replace with */ replaceTimezone(timezone) { this._modifyContent(); this._innerValue = ICAL__default.default.Time.fromData({ year: this.year, month: this.month, day: this.day, hour: this.hour, minute: this.minute, second: this.second, isDate: this.isDate }, timezone.toICALTimezone()); } /** * Calculates the UTC offset of the date-time-value in its timezone * * @return {number} */ utcOffset() { return this._innerValue.utcOffset(); } /** * Check if this is an event with floating time * * @return {boolean} */ isFloatingTime() { return this._innerValue.zone.tzid === "floating"; } /** * clones this value * * @return {DateTimeValue} */ clone() { return DateTimeValue.fromICALJs(this._innerValue.clone()); } /** * Create a new DateTimeValue object from an ICAL.Time object * * @param {ICAL.Time} icalValue The ical.js Date value to initialise from * @return {DateTimeValue} */ static fromICALJs(icalValue) { return new DateTimeValue(icalValue); } /** * Creates a new DateTimeValue object based on a vanilla javascript object * * @param {Date} jsDate The JavaScript date to initialise from * @param {boolean=} useUTC Whether or not to treat it as UTC * @return {DateTimeValue} */ static fromJSDate(jsDate, useUTC = false) { const icalValue = ICAL__default.default.Time.fromJSDate(jsDate, useUTC); return DateTimeValue.fromICALJs(icalValue); } /** * Creates a new DateTimeValue object based on simple parameters * * @param {object} data The destructuring object * @param {number=} data.year Amount of years to set * @param {number=} data.month Amount of month to set (1-based) * @param {number=} data.day Amount of days to set * @param {number=} data.hour Amount of hours to set * @param {number=} data.minute Amount of minutes to set * @param {number=} data.second Amount of seconds to set * @param {boolean=} data.isDate Whether this is a date or date-time * @param {Timezone=} timezone The timezone of the DateTimeValue * @return {DateTimeValue} */ static fromData(data, timezone) { const icalValue = ICAL__default.default.Time.fromData(data, timezone ? timezone.toICALTimezone() : void 0); return DateTimeValue.fromICALJs(icalValue); } } DateTimeValue.SUNDAY = ICAL__default.default.Time.SUNDAY; DateTimeValue.MONDAY = ICAL__default.default.Time.MONDAY; DateTimeValue.TUESDAY = ICAL__default.default.Time.TUESDAY; DateTimeValue.WEDNESDAY = ICAL__default.default.Time.WEDNESDAY; DateTimeValue.THURSDAY = ICAL__default.default.Time.THURSDAY; DateTimeValue.FRIDAY = ICAL__default.default.Time.FRIDAY; DateTimeValue.SATURDAY = ICAL__default.default.Time.SATURDAY; DateTimeValue.DEFAULT_WEEK_START = DateTimeValue.MONDAY; /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class PeriodValue extends AbstractValue { /** * @inheritDoc */ constructor(...args) { super(...args); this._start = DateTimeValue.fromICALJs(this._innerValue.start); this._end = null; this._duration = null; } /** * Gets the start of the period-value * * @return {DateTimeValue} */ get start() { return this._start; } /** * Sets the start of the period-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {DateTimeValue} start The start of the period */ set start(start) { this._modifyContent(); this._start = start; this._innerValue.start = start.toICALJs(); } /** * Gets the end of the period-value * * @return {DateTimeValue} */ get end() { if (!this._end) { if (this._duration) { this._duration.lock(); this._duration = null; } this._innerValue.end = this._innerValue.getEnd(); this._end = DateTimeValue.fromICALJs(this._innerValue.end); this._innerValue.duration = null; if (this.isLocked()) { this._end.lock(); } } return this._end; } /** * Sets the end of the period-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {DateTimeValue} end The end of the period */ set end(end) { this._modifyContent(); this._innerValue.duration = null; this._innerValue.end = end.toICALJs(); this._end = end; } /** * Gets the duration of the period-value * The value is automatically locked. * If you want to edit the value, clone it and it as new duration * * @return {DurationValue} */ get duration() { if (!this._duration) { if (this._end) { this._end.lock(); this._end = null; } this._innerValue.duration = this._innerValue.getDuration(); this._duration = DurationValue.fromICALJs(this._innerValue.duration); this._innerValue.end = null; if (this.isLocked()) { this._duration.lock(); } } return this._duration; } /** * Sets the duration of the period-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {DurationValue} duration The duration to set */ set duration(duration) { this._modifyContent(); this._innerValue.end = null; this._innerValue.duration = duration.toICALJs(); this._duration = duration; } /** * @inheritDoc */ lock() { super.lock(); this.start.lock(); if (this._end) { this._end.lock(); } if (this._duration) { this._duration.lock(); } } /** * @inheritDoc */ unlock() { super.unlock(); this.start.unlock(); if (this._end) { this._end.unlock(); } if (this._duration) { this._duration.unlock(); } } /** * clones this value * * @return {PeriodValue} */ clone() { return PeriodValue.fromICALJs(this._innerValue.clone()); } /** * Create a new PeriodValue object from a ICAL.Period object * * @param {ICAL.Period} icalValue The ical.js period value to initialise from * @return {PeriodValue} */ static fromICALJs(icalValue) { return new PeriodValue(icalValue); } /** * Create a new PeriodValue object from start and end * * @param {object} data The destructuring object * @param {DateTimeValue} data.start The start of the period * @param {DateTimeValue} data.end The end of the period * @return {PeriodValue} */ static fromDataWithEnd(data) { const icalPeriod = ICAL__default.default.Period.fromData({ start: data.start.toICALJs(), end: data.end.toICALJs() }); return PeriodValue.fromICALJs(icalPeriod); } /** * Create a new PeriodValue object from start and duration * * @param {object} data The destructuring object * @param {DateTimeValue} data.start The start of the period * @param {DurationValue} data.duration The duration of the period * @return {PeriodValue} */ static fromDataWithDuration(data) { const icalPeriod = ICAL__default.default.Period.fromData({ start: data.start.toICALJs(), duration: data.duration.toICALJs() }); return PeriodValue.fromICALJs(icalPeriod); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ const ALLOWED_FREQ = ["SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY"]; class RecurValue extends AbstractValue { /** * Constructor * * @param {ICAL.Recur} icalValue The ical.js rrule value * @param {DateTimeValue?} until The Until date */ constructor(icalValue, until) { super(icalValue); this._until = until; } /** * Gets the stored interval of this recurrence rule * * @return {number} */ get interval() { return this._innerValue.interval; } /** * Sets the stored interval of this recurrence rule * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {number} interval New Interval to set */ set interval(interval) { this._modifyContent(); this._innerValue.interval = parseInt(interval, 10); } /** * Gets the weekstart used to calculate the recurrence expansion * * @return {number} */ get weekStart() { return this._innerValue.wkst; } /** * Sets the weekstart used to calculate the recurrence expansion * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if weekstart out of range * @param {number} weekStart New start of week to set */ set weekStart(weekStart) { this._modifyContent(); if (weekStart < DateTimeValue.SUNDAY || weekStart > DateTimeValue.SATURDAY) { throw new TypeError("Weekstart out of range"); } this._innerValue.wkst = weekStart; } /** * Gets the until value if set * The value is automatically locked. * If you want to edit the value, clone it and it as new until * * @return {null|DateTimeValue} */ get until() { if (!this._until && this._innerValue.until) { this._until = DateTimeValue.fromICALJs(this._innerValue.until); } return this._until; } /** * Sets the until value, automatically removes count * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {DateTimeValue} until New until date to set */ set until(until) { this._modifyContent(); if (this._until) { this._until.lock(); } this._until = until; this._innerValue.count = null; this._innerValue.until = until.toICALJs(); } /** * Gets the count value if set * * @return {null | number} */ get count() { return this._innerValue.count; } /** * Sets the count value, automatically removes until * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {number} count New occurrence limit to set */ set count(count) { this._modifyContent(); if (this._until) { this._until.lock(); this._until = null; } this._innerValue.until = null; this._innerValue.count = parseInt(count, 10); } /** * Gets the frequency of the recurrence rule * * @return {string} see */ get frequency() { return this._innerValue.freq; } /** * Sets the frequency of the recurrence rule * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if frequency is unknown * @param {string} freq New frequency to set */ set frequency(freq) { this._modifyContent(); if (!ALLOWED_FREQ.includes(freq)) { throw new TypeError("Unknown frequency"); } this._innerValue.freq = freq; } /** * Modifies this recurrence-value to unset count and until */ setToInfinite() { this._modifyContent(); if (this._until) { this._until.lock(); this._until = null; } this._innerValue.until = null; this._innerValue.count = null; } /** * Checks whether the stored rule is finite * * @return {boolean} */ isFinite() { return this._innerValue.isFinite(); } /** * Checks whether the recurrence rule is limited by count * * @return {boolean} */ isByCount() { return this._innerValue.isByCount(); } /** * Adds a part to a component to the recurrence-rule * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {string} componentName The name of the recurrence-component to add * @param {string | number} value The value to add */ addComponent(componentName, value) { this._modifyContent(); this._innerValue.addComponent(componentName, value); } /** * Sets / overwrites a component to the recurrence-rule * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {string} componentName The name of the component to set * @param {number[] | string[]} value The value to set */ setComponent(componentName, value) { this._modifyContent(); if (value.length === 0) { delete this._innerValue.parts[componentName.toUpperCase()]; } else { this._innerValue.setComponent(componentName, value); } } /** * Removes all parts of a component * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {string} componentName The name of the component to remove */ removeComponent(componentName) { delete this._innerValue.parts[uc(componentName)]; } /** * Gets all parts of a component * * @param {string} componentName The name of the component to get * @return {Array} */ getComponent(componentName) { return this._innerValue.getComponent(componentName); } /** * Checks if this recurrence rule is valid according to RFC 5545 * * @return {boolean} */ isRuleValid() { return true; } /** * @inheritDoc */ lock() { super.lock(); if (this._until) { this._until.lock(); } } /** * @inheritDoc */ unlock() { super.unlock(); if (this._until) { this._until.unlock(); } } /** * clones this value * * @return {RecurValue} */ clone() { return RecurValue.fromICALJs(this._innerValue.clone()); } /** * Create a new RecurValue object from a ICAL.Recur object * * @param {ICAL.Recur} icalValue The ICAL.JS Recur value * @param {DateTimeValue?} until The Until date * @return {RecurValue} */ static fromICALJs(icalValue, until = null) { return new RecurValue(icalValue, until); } /** * Create a new RecurValue object from a data object * * @param {object} data The destructuring object * @param {string=} data.freq FREQ part of RRULE * @param {number=} data.interval INTERVAL part of RRULE * @param {number=} data.wkst WEEKSTART part of RRULE * @param {DateTimeValue=} data.until UNTIL part of RRULE * @param {number=} data.count COUNT part of RRULE * @param {number[]=} data.bysecond BYSECOND part of RRULE * @param {number[]=} data.byminute BYMINUTE part of RRULE * @param {number[]=} data.byhour BYHOUR part of RRULE * @param {string[]=} data.byday BYDAY part of RRULE * @param {number[]=} data.bymonthday BYMONTHDAY part of RRULE * @param {number[]=} data.byyearday BYYEARDAY part of RRULE * @param {number[]=} data.byweekno BYWEEKNO part of RRULE * @param {number[]=} data.bymonth BYMONTH part of RRULE * @param {number[]=} data.bysetpos BYSETPOS part of RRULE * @return {RecurValue} */ static fromData(data) { let until = null; if (data.until) { until = data.until; data.until = data.until.toICALJs(); } const icalRecur = ICAL__default.default.Recur.fromData(data); return RecurValue.fromICALJs(icalRecur, until); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class UTCOffsetValue extends AbstractValue { /** * Gets the hour part of the offset-value * * @return {number} */ get hours() { return this._innerValue.hours; } /** * Sets the hour part of the offset-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {number} hours - New hours to set */ set hours(hours) { this._modifyContent(); this._innerValue.hours = hours; } /** * Gets the minute part of the offset-value * * @return {number} */ get minutes() { return this._innerValue.minutes; } /** * Sets the minute part of the offset-value * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {number} minutes - New minutes to set */ set minutes(minutes) { this._modifyContent(); this._innerValue.minutes = minutes; } /** * Gets the factor * * @return {number} */ get factor() { return this._innerValue.factor; } /** * Sets the factor * * @throws {ModificationNotAllowedError} if value is locked for modification * @throws {TypeError} if factor is neither 1 nor -1 * @param {number} factor - New factor to set, 1 for positive, -1 for negative */ set factor(factor) { this._modifyContent(); if (factor !== 1 && factor !== -1) { throw new TypeError("Factor may only be set to 1 or -1"); } this._innerValue.factor = factor; } /** * Gets the total amount of seconds * * @return {number} */ get totalSeconds() { return this._innerValue.toSeconds(); } /** * Sets the total amount of seconds * * @throws {ModificationNotAllowedError} if value is locked for modification * @param {number} totalSeconds - New number of total seconds to set */ set totalSeconds(totalSeconds) { this._modifyContent(); this._innerValue.fromSeconds(totalSeconds); } /** * Compares this UTCOffset to another one * * @param {UTCOffsetValue} other - The other UTCOffsetValue to compare with * @return {number} -1, 0 or 1 for less/equal/greater */ compare(other) { return this._innerValue.compare(other.toICALJs()); } /** * Clones this value * * @return {UTCOffsetValue} */ clone() { return UTCOffsetValue.fromICALJs(this._innerValue.clone()); } /** * Create a new UTCOffsetValue object from a ICAL.UTCOffset object * * @param {ICAL.UtcOffset} icalValue - The ICAL.UtcOffset object to initialize this object from * @return {UTCOffsetValue} */ static fromICALJs(icalValue) { return new UTCOffsetValue(icalValue); } /** * Create a new UTCOffsetValue object from a data object * * @param {object} data - Object with data to create UTCOffsetValue object from * @param {number=} data.hours - The number of hours to set * @param {number=} data.minutes - The number of minutes to set * @param {number=} data.factor - The factor to use, 1 for positive, -1 for negative * @return {UTCOffsetValue} */ static fromData(data) { const icalUTCOffset = new ICAL__default.default.UtcOffset(); icalUTCOffset.fromData(data); return UTCOffsetValue.fromICALJs(icalUTCOffset); } /** * Create a new UTCOffsetValue object from an amount of seconds *w * * @param {number} seconds - The total number of seconds to create the UTCOffsetValue object from * @return {UTCOffsetValue} */ static fromSeconds(seconds) { const icalUTCOffset = ICAL__default.default.UtcOffset.fromSeconds(seconds); return UTCOffsetValue.fromICALJs(icalUTCOffset); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class UnknownICALTypeError extends Error { } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @author Richard Steinmetz * * @license AGPL-3.0-or-later * * 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 . * */ function getConstructorForICALType(icaltype) { switch (lc(icaltype)) { case "binary": return BinaryValue; case "date": case "date-time": return DateTimeValue; case "duration": return DurationValue; case "period": return PeriodValue; case "recur": return RecurValue; case "utc-offset": return UTCOffsetValue; default: throw new UnknownICALTypeError(); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class Property extends observerTrait(lockableTrait(class { })) { /** * Constructor * * @param {string} name The name of the property * @param {string | number | AbstractValue | string[] | number[] | AbstractValue[] | null} value The value of the property * @param {Parameter[] | [string][]} parameters Array of parameters * @param {CalendarComponent|null} root The root of the calendar-document * @param {AbstractComponent|null} parent The parent-element of this property */ constructor(name, value = null, parameters = [], root = null, parent = null) { super(); this._name = uc(name); this._value = value; this._parameters = /* @__PURE__ */ new Map(); this._root = root; this._parent = parent; this._setParametersFromConstructor(parameters); if (value instanceof AbstractValue) { value.subscribe(() => this._notifySubscribers()); } } /** * Get property name * * @readonly * @return {string} */ get name() { return this._name; } /** * Get parameter value * * @return {string | number | AbstractValue | string[] | number[] | AbstractValue[] | null} */ get value() { return this._value; } /** * Set new parameter value * * @param {string | number | AbstractValue | string[] | number[] | AbstractValue[] | null} value The value of the property * @throws {ModificationNotAllowedError} if property is locked for modification */ set value(value) { this._modifyContent(); this._value = value; if (value instanceof AbstractValue) { value.subscribe(() => this._notifySubscribers()); } } /** * Gets the root of this property * * @return {CalendarComponent|null} */ get root() { return this._root; } /** * Sets the root of this property * * @param {CalendarComponent|null} root The root of the calendar-document * @throws {ModificationNotAllowedError} if property is locked for modification */ set root(root) { this._modify(); this._root = root; } /** * Gets the direct parent element of this property * * @return {AbstractComponent} */ get parent() { return this._parent; } /** * Sets the direct parent element of this property * * @param {AbstractComponent|null} parent The parent element of this property * @throws {ModificationNotAllowedError} if property is locked for modification */ set parent(parent) { this._modify(); this._parent = parent; } /** * Gets the first value of this property * * @return {null | string | number | AbstractValue} */ getFirstValue() { if (!this.isMultiValue()) { return this.value; } else { if (this.value.length > 0) { return this.value[0]; } } return null; } /** * Gets an iterator over all values */ *getValueIterator() { if (this.isMultiValue()) { yield* this.value.slice()[Symbol.iterator](); } else { yield this.value; } } /** * Adds a value to the multi-value property * * @param {string | AbstractValue} value Value to add */ addValue(value) { if (!this.isMultiValue()) { throw new TypeError("This is not a multivalue property"); } this._modifyContent(); this.value.push(value); } /** * Checks if a value is inside this multi-value property * * @param {string | AbstractValue} value Value to check for * @return {boolean} */ hasValue(value) { if (!this.isMultiValue()) { throw new TypeError("This is not a multivalue property"); } return this.value.includes(value); } /** * Removes a value from this multi-value property * * @param {string | AbstractValue} value Value to remove */ removeValue(value) { if (!this.hasValue(value)) { return; } this._modifyContent(); const index = this.value.indexOf(value); this.value.splice(index, 1); } /** * Sets a parameter on this property * * @param {Parameter} parameter The parameter to set * @throws {ModificationNotAllowedError} if property is locked for modification */ setParameter(parameter) { this._modify(); this._parameters.set(parameter.name, parameter); parameter.subscribe(() => this._notifySubscribers()); } /** * Gets a parameter on this property by its name * * @param {string} parameterName Name of the parameter to get * @return {Parameter} */ getParameter(parameterName) { return this._parameters.get(uc(parameterName)); } /** * Gets an iterator over all available parameters */ *getParametersIterator() { yield* this._parameters.values(); } /** * Get first value of a parameter * * @param {string} parameterName Name of the parameter * @return {null | string} */ getParameterFirstValue(parameterName) { const parameter = this.getParameter(parameterName); if (parameter instanceof Parameter) { if (parameter.isMultiValue()) { return parameter.value[0]; } else { return parameter.value; } } return null; } /** * Returns whether a parameter exists on this property * * @param {string} parameterName Name of the parameter * @return {boolean} */ hasParameter(parameterName) { return this._parameters.has(uc(parameterName)); } /** * Deletes a parameter on this property * * @param {string} parameterName Name of the parameter * @throws {ModificationNotAllowedError} if property is locked for modification */ deleteParameter(parameterName) { this._modify(); this._parameters.delete(uc(parameterName)); } /** * update a parameter if it exists, * create a new one if it doesn't * * @param {string} parameterName Name of the parameter * @param {string|Array|null} value Value to set * @throws {ModificationNotAllowedError} if property is locked for modification */ updateParameterIfExist(parameterName, value) { this._modify(); if (this.hasParameter(parameterName)) { const parameter = this.getParameter(parameterName); parameter.value = value; } else { const parameter = new Parameter(uc(parameterName), value); this.setParameter(parameter); } } /** * Returns whether or not the value is a multivalue * * @return {boolean} */ isMultiValue() { return Array.isArray(this._value); } /** * Returns whether or not this valus is decorated * * @return {boolean} */ isDecoratedValue() { if (this.isMultiValue()) { return this._value[0] instanceof AbstractValue; } else { return this._value instanceof AbstractValue; } } /** * Marks this parameter is immutable * locks it against further modification */ lock() { super.lock(); for (const parameter of this.getParametersIterator()) { parameter.lock(); } if (this.isDecoratedValue()) { for (const value of this.getValueIterator()) { value.lock(); } } } /** * Marks this parameter as mutable * allowing further modification */ unlock() { super.unlock(); for (const parameter of this.getParametersIterator()) { parameter.unlock(); } if (this.isDecoratedValue()) { for (const value of this.getValueIterator()) { value.unlock(); } } } /** * Creates a copy of this parameter * * @return {Property} */ clone() { const parameters = []; for (const parameter of this.getParametersIterator()) { parameters.push(parameter.clone()); } return new this.constructor(this.name, this._cloneValue(), parameters, this.root, this.parent); } /** * Copies the values of this property * * @return {string | number | AbstractValue | string[] | number[] | AbstractValue[] | null} * @protected */ _cloneValue() { if (this.isDecoratedValue()) { if (this.isMultiValue()) { return this._value.map((val) => val.clone()); } else { return this._value.clone(); } } else { if (this.isMultiValue()) { return this._value.slice(); } else { return this._value; } } } /** * Sets parameters from the constructor * * @param {Parameter[] | [string][]} parameters Array of parameters to set * @private */ _setParametersFromConstructor(parameters) { parameters.forEach((parameter) => { if (!(parameter instanceof Parameter)) { parameter = new Parameter(parameter[0], parameter[1]); } this.setParameter(parameter); }); } /** * Creates a new Component based on an ical object * * @param {ICAL.Property} icalProperty The ical.js property to initialise from * @param {CalendarComponent=} root The root of the calendar-document * @param {AbstractComponent=} parent The parent element of this property * @return {Property} */ static fromICALJs(icalProperty, root = null, parent = null) { if (!(icalProperty instanceof ICAL__default.default.Property)) { throw new ExpectedICalJSError(); } let value; if (icalProperty.isDecorated) { const constructor = getConstructorForICALType(icalProperty.getFirstValue().icaltype); if (icalProperty.isMultiValue) { value = icalProperty.getValues().map((val) => constructor.fromICALJs(val)); } else { value = constructor.fromICALJs(icalProperty.getFirstValue()); } } else { if (icalProperty.isMultiValue) { value = icalProperty.getValues(); } else { value = icalProperty.getFirstValue(); } } const parameters = []; const paramNames = Object.keys(Object.assign({}, icalProperty.toJSON()[1])); paramNames.forEach((paramName) => { if (uc(paramName) === "TZID") { return; } parameters.push([paramName, icalProperty.getParameter(paramName)]); }); return new this(icalProperty.name, value, parameters, root, parent); } /** * Returns an ICAL.js property based on this Property * * @return {ICAL.Property} */ toICALJs() { const icalProperty = createProperty(lc(this.name)); if (this.isMultiValue()) { if (this.isDecoratedValue()) { icalProperty.setValues(this.value.map((val) => val.toICALJs())); } else { icalProperty.setValues(this.value); } } else { if (this.isDecoratedValue()) { icalProperty.setValue(this.value.toICALJs()); } else { icalProperty.setValue(this.value); } } for (const parameter of this.getParametersIterator()) { icalProperty.setParameter(lc(parameter.name), parameter.value); } const firstValue = this.getFirstValue(); if (firstValue instanceof DateTimeValue && firstValue.timezoneId !== "floating" && firstValue.timezoneId !== "UTC" && !firstValue.isDate) { icalProperty.setParameter("tzid", firstValue.timezoneId); } return icalProperty; } /** * @inheritDoc */ _modifyContent() { super._modifyContent(); this._notifySubscribers(); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class AttachmentProperty extends Property { /** * Gets the format-type of this attachment * * @return {string} */ get formatType() { return this.getParameterFirstValue("FMTTYPE"); } /** * Sets the format-type of this attachment * * @param {string} fmtType Mime-type of attachment */ set formatType(fmtType) { this.updateParameterIfExist("FMTTYPE", fmtType); } /** * Gets the uri of this attachment * * @return {string | null} */ get uri() { if (this._value instanceof BinaryValue) { return null; } return this._value; } /** * Sets the uri of this attachment * * @param {string} uri Link to attachment if applicable */ set uri(uri) { this.value = uri; } /** * Gets the encoding of this attachment * * @return {string|null} */ get encoding() { if (this._value instanceof BinaryValue) { return "BASE64"; } return null; } /** * Gets the data stored in this attachment * * @return {string | null} */ get data() { if (this._value instanceof BinaryValue) { return this._value.value; } return null; } /** * Sets the data stored in this attachment * * @param {string} data The data of the attachment */ set data(data) { if (this.value instanceof BinaryValue) { this.value.value = data; } else { this.value = BinaryValue.fromDecodedValue(data); } } /** * @inheritDoc */ toICALJs() { const icalProperty = super.toICALJs(); if (this._value instanceof BinaryValue && this.getParameterFirstValue("ENCODING") !== "BASE64") { icalProperty.setParameter("ENCODING", "BASE64"); } return icalProperty; } /** * Creates a new AttachmentProperty based on data * * @param {string} data The data of the attachment * @param {string=} formatType The mime-type of the data * @return {AttachmentProperty} */ static fromData(data, formatType = null) { const binaryValue = BinaryValue.fromDecodedValue(data); const property = new AttachmentProperty("ATTACH", binaryValue); if (formatType) { property.formatType = formatType; } return property; } /** * Creates a new AttachmentProperty based on a link * * @param {string} uri The URI for the attachment * @param {string=} formatType The mime-type of the uri * @return {AttachmentProperty} */ static fromLink(uri, formatType = null) { const property = new AttachmentProperty("ATTACH", uri); if (formatType) { property.formatType = formatType; } return property; } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * @author Richard Steinmetz * * @license AGPL-3.0-or-later * * 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 . * */ class AttendeeProperty extends Property { /** * Returns the role of the attendee. * * @return {string} */ get role() { const allowed = ["CHAIR", "REQ-PARTICIPANT", "OPT-PARTICIPANT", "NON-PARTICIPANT"]; const defaultValue = "REQ-PARTICIPANT"; if (this.hasParameter("ROLE")) { const value = this.getParameterFirstValue("ROLE"); if (allowed.includes(value)) { return value; } } return defaultValue; } /** * Sets new role of the attendee * * @param {string} role The role of the attendee (e.g. CHAIR, REQ-PARTICIPANT) */ set role(role) { this.updateParameterIfExist("ROLE", role); } /** * Returns the calendar-user-type of an attendee * * @return {string} */ get userType() { const allowed = ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", "UNKNOWN"]; if (!this.hasParameter("CUTYPE")) { return "INDIVIDUAL"; } else { const value = this.getParameterFirstValue("CUTYPE"); if (allowed.includes(value)) { return value; } return "UNKNOWN"; } } /** * Sets new calendar-user-type of attendee * * @param {string} userType The type of user (e.g. INDIVIDUAL, GROUP) */ set userType(userType) { this.updateParameterIfExist("CUTYPE", userType); } /** * Returns the "Répondez s'il vous plaît" value for attendee * * @return {boolean} */ get rsvp() { if (!this.hasParameter("RSVP")) { return false; } else { const value = this.getParameterFirstValue("RSVP"); return uc(value) === "TRUE"; } } /** * Updates the "Répondez s'il vous plaît" value for attendee * * @param {boolean} rsvp Whether or not to send out an invitation */ set rsvp(rsvp) { this.updateParameterIfExist("RSVP", rsvp ? "TRUE" : "FALSE"); } /** * Returns the common-name of the attendee * * @return {string|null} */ get commonName() { return this.getParameterFirstValue("CN"); } /** * Sets a new common-name of the attendee * * @param {string} commonName The display name of the attendee */ set commonName(commonName) { this.updateParameterIfExist("CN", commonName); } /** * Returns the participation-status of the attendee * * @return {string} */ get participationStatus() { let vobjectType; if (this.parent) { vobjectType = this.parent.name; } else { vobjectType = "VEVENT"; } const allowed = { VEVENT: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE", "DELEGATED"], VJOURNAL: ["NEEDS-ACTION", "ACCEPTED", "DECLINED"], VTODO: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE", "DELEGATED", "COMPLETED", "IN-PROCESS"] }; if (!this.hasParameter("PARTSTAT")) { return "NEEDS-ACTION"; } else { const value = this.getParameterFirstValue("PARTSTAT"); if (allowed[vobjectType].includes(value)) { return value; } return "NEEDS-ACTION"; } } /** * Sets a new participation-status of the attendee * * @param {string} participationStatus The participation status (e.g. ACCEPTED, DECLINED) */ set participationStatus(participationStatus) { this.updateParameterIfExist("PARTSTAT", participationStatus); } /** * Gets this attendee's language * * @return {string} */ get language() { return this.getParameterFirstValue("LANGUAGE"); } /** * Sets this attendee's language * This can be used to influence the language of the invitation email * * @param {string} language The preferred language of the attendee */ set language(language) { this.updateParameterIfExist("LANGUAGE", language); } /** * Gets the email of the attendee * * @return {string} */ get email() { return this.value; } /** * Sets the email address of the attendee * * @param {string} email The e-email address of the attendee */ set email(email) { this.value = startStringWith(email, "mailto:"); } /** * Gets the email addresses of groups the attendee is a part of * * @return {string[]|null} The email addresses of the groups */ get member() { return this.getParameter("MEMBER")?.value ?? null; } /** * Sets the email addresses of groups the attendee is a part of * * @param {string[]} members The email addresses of the groups */ set member(members) { members = members.map((member) => startStringWith(member, "mailto:")); this.updateParameterIfExist("MEMBER", members); } /** * Is this attendee the organizer? * * @return {boolean} */ isOrganizer() { return this._name === "ORGANIZER"; } /** * Creates a new AttendeeProperty from name and email * * @param {string} name The display name * @param {string} email The email address * @param {boolean=} isOrganizer Whether this is the organizer or an attendee * @return {AttendeeProperty} */ static fromNameAndEMail(name, email, isOrganizer = false) { const propertyName = isOrganizer ? "ORGANIZER" : "ATTENDEE"; email = startStringWith(email, "mailto:"); return new AttendeeProperty(propertyName, email, [["CN", name]]); } /** * Creates a new AttendeeProperty from name, email, role, userType and rsvp * * @param {string} name The display name * @param {string} email The email address * @param {string} role The role * @param {string} userType The type of user * @param {boolean} rsvp Whether to send out an invitation * @param {boolean=} isOrganizer Whether this is the organizer or an attendee * @return {AttendeeProperty} */ static fromNameEMailRoleUserTypeAndRSVP(name, email, role, userType, rsvp, isOrganizer = false) { const propertyName = isOrganizer ? "ORGANIZER" : "ATTENDEE"; email = startStringWith(email, "mailto:"); return new AttendeeProperty(propertyName, email, [ ["CN", name], ["ROLE", role], ["CUTYPE", userType], ["RSVP", rsvp ? "TRUE" : "FALSE"] ]); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ ICAL__default.default.design.icalendar.property.conference = { defaultType: "uri" }; ICAL__default.default.design.icalendar.param.feature = { valueType: "cal-address", multiValue: "," }; class ConferenceProperty extends Property { /** * Iterator that iterates over all supported features * of the conference system */ *getFeatureIterator() { if (!this.hasParameter("FEATURE")) { return; } const parameter = this.getParameter("FEATURE"); yield* parameter.getValueIterator(); } /** * Lists all supported features of the conference system * * @return {string[]} */ listAllFeatures() { if (!this.hasParameter("FEATURE")) { return []; } return this.getParameter("FEATURE").value.slice(); } /** * Adds a supported feature to the conference system * * @param {string} featureToAdd Feature to add */ addFeature(featureToAdd) { this._modify(); if (!this.hasParameter("FEATURE")) { this.updateParameterIfExist("FEATURE", [featureToAdd]); } else { if (this.hasFeature(featureToAdd)) { return; } const parameter = this.getParameter("FEATURE"); parameter.value.push(featureToAdd); } } /** * Removes a supported feature * * @param {string} feature The feature to remove */ removeFeature(feature) { this._modify(); if (!this.hasFeature(feature)) { return; } const parameter = this.getParameter("FEATURE"); const index = parameter.value.indexOf(feature); parameter.value.splice(index, 1); } /** * Removes all supported features from this conference system */ clearAllFeatures() { this.deleteParameter("FEATURE"); } /** * Check if this conference system supports a feature * * @param {string} feature The feature to check * @return {boolean} */ hasFeature(feature) { if (!this.hasParameter("FEATURE")) { return false; } const parameter = this.getParameter("FEATURE"); if (!Array.isArray(parameter.value)) { return false; } return parameter.value.includes(feature); } /** * Gets label for the conference system * * @return {string} */ get label() { return this.getParameterFirstValue("LABEL"); } /** * Updates the label for the conference system * * @param {string} label The label to set */ set label(label) { this.updateParameterIfExist("LABEL", label); } /** * Gets the uri for this conference system */ get uri() { return this.value; } /** * Sets the uri for this conference system * * @param {string} uri The URI to set */ set uri(uri) { this.value = uri; } /** * @inheritDoc */ toICALJs() { const icalProperty = super.toICALJs(); icalProperty.setParameter("value", "URI"); return icalProperty; } /** * Creates a new ConferenceProperty based on URI, label and features * * @param {string} uri URI of the Conference * @param {string=} label Label of the conference * @param {string[]=} features Features of the conference * @return {ConferenceProperty} */ static fromURILabelAndFeatures(uri, label = null, features = null) { const property = new ConferenceProperty("CONFERENCE", uri); if (label) { property.updateParameterIfExist("label", label); } if (features) { property.updateParameterIfExist("feature", features); } return property; } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class FreeBusyProperty extends Property { /** * Gets the type of this FreeBusyProperty * * @return {string} */ get type() { const allowed = ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"]; const defaultValue = "BUSY"; if (this.hasParameter("FBTYPE")) { const value = this.getParameterFirstValue("FBTYPE"); if (allowed.includes(value)) { return value; } } return defaultValue; } /** * Sets the type of this FreeBusyProperty * * @param {string} type The type of information (e.g. FREE, BUSY, etc.) */ set type(type) { this.updateParameterIfExist("FBTYPE", type); } /** * Creates a new FreeBusyProperty based on period and type * * @param {PeriodValue} period The period for FreeBusy Information * @param {string} type The type of the period * @return {FreeBusyProperty} */ static fromPeriodAndType(period, type) { return new FreeBusyProperty("FREEBUSY", period, [["fbtype", type]]); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class GeoProperty extends Property { /** * @inheritDoc */ constructor(name, value = [0, 0], parameters = [], root = null, parent = null) { super(name, value, parameters, root, parent); } /** * Gets the latitude stored in this property * * @return {number} */ get latitude() { return this._value[0]; } /** * Sets the latitude stored in this property * * @param {string | number} lat Latitude */ set latitude(lat) { this._modifyContent(); if (typeof lat !== "number") { lat = parseFloat(lat); } this._value[0] = lat; } /** * Gets the longitude stored in this property */ get longitude() { return this._value[1]; } /** * Sets the longitude stored in this property * * @param {string | number} long Longitude */ set longitude(long) { this._modifyContent(); if (typeof long !== "number") { long = parseFloat(long); } this._value[1] = long; } /** * @inheritDoc * * TODO: this is an ugly hack right now. * As soon as the value is an array, we assume it's multivalue * but GEO is a (the one and only besides request-status) structured value and is also * stored inside an array. * * Calling icalProperty.setValues will throw an error */ toICALJs() { const icalProperty = createProperty(lc(this.name)); icalProperty.setValue(this.value); this._parameters.forEach((parameter) => { icalProperty.setParameter(lc(parameter.name), parameter.value); }); return icalProperty; } /** * Creates a new GeoProperty based on a latitude and a longitude value * * @param {number} lat Latitude * @param {number} long Longitude * @return {GeoProperty} */ static fromPosition(lat, long) { return new GeoProperty("GEO", [lat, long]); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ImageProperty extends AttachmentProperty { /** * Gets the image-type */ get display() { return this.getParameterFirstValue("DISPLAY") || "BADGE"; } /** * Gets the image-type * * @param {string} display The display-type image is optimized for */ set display(display) { this.updateParameterIfExist("DISPLAY", display); } /** * Creates a new ImageProperty based on data * * @param {string} data The data of the image * @param {string=} display The display-type it's optimized for * @param {string=} formatType The mime-type of the image * @return {ImageProperty} */ static fromData(data, display = null, formatType = null) { const binaryValue = BinaryValue.fromDecodedValue(data); const property = new ImageProperty("IMAGE", binaryValue); if (display) { property.display = display; } if (formatType) { property.formatType = formatType; } return property; } /** * Creates a new ImageProperty based on a link * * @param {string} uri The uri of the image * @param {string=} display The display-type it's optimized for * @param {string=} formatType The mime-type of the image * @return {ImageProperty} */ static fromLink(uri, display = null, formatType = null) { const property = new ImageProperty("IMAGE", uri); if (display) { property.display = display; } if (formatType) { property.formatType = formatType; } return property; } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class RelationProperty extends Property { /** * Get's the relation-type of this related-to property * * @return {string} */ get relationType() { const allowed = ["PARENT", "CHILD", "SIBLING"]; const defaultValue = "PARENT"; if (!this.hasParameter("RELTYPE")) { return defaultValue; } else { const value = this.getParameterFirstValue("RELTYPE"); if (allowed.includes(value)) { return value; } return defaultValue; } } /** * Sets a new relation type * * @param {string} relationType The type of relation (e.g. SIBLING, PARENT, etc.) */ set relationType(relationType) { this.updateParameterIfExist("RELTYPE", relationType); } /** * Gets Id of related object * * @return {string} */ get relatedId() { return this.value; } /** * Sets a new related id * * @param {string} relatedId The Id of the related document */ set relatedId(relatedId) { this.value = relatedId; } /** * Creates a new RELATED-TO property based on a relation-type and id * * @param {string} relType The type of the relation (e.g. SIBLING, CHILD) * @param {string} relId The Id of the related document * @return {RelationProperty} */ static fromRelTypeAndId(relType, relId) { return new RelationProperty("RELATED-TO", relId, [["RELTYPE", relType]]); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class RequestStatusProperty extends Property { /** * @inheritDoc */ constructor(name, value = ["1", "Pending"], parameters = [], root = null, parent = null) { super(name, value, parameters, root, parent); } /** * Gets the status code of the request status * * @return {number} */ get statusCode() { return parseFloat(this.value[0]); } /** * Sets the status code of the request status * * @param {number} statusCode The statusCode of the request */ set statusCode(statusCode) { this._modifyContent(); this.value[0] = statusCode.toString(); if (statusCode === Math.floor(statusCode)) { this.value[0] += ".0"; } } /** * Gets the status message of the request status * * @return {string} */ get statusMessage() { return this.value[1]; } /** * Sets the status message of the request status * * @param {string} statusMessage The message of the request */ set statusMessage(statusMessage) { this._modifyContent(); this.value[1] = statusMessage; } /** * Gets the exception data of the request status if available * * @return {null | string} */ get exceptionData() { if (!this.value[2]) { return null; } return this.value[2]; } /** * Sets the exception dtat of the request status * * @param {string} exceptionData The additional exception-data */ set exceptionData(exceptionData) { this._modifyContent(); this.value[2] = exceptionData; } /** * Check if request is pending * * @return {boolean} */ isPending() { return this.statusCode >= 1 && this.statusCode < 2; } /** * Check if request was successful * * @return {boolean} */ isSuccessful() { return this.statusCode >= 2 && this.statusCode < 3; } /** * Check if a client error occurred * * @return {boolean} */ isClientError() { return this.statusCode >= 3 && this.statusCode < 4; } /** * Check if a scheduling error occurred * * @return {boolean} */ isSchedulingError() { return this.statusCode >= 4 && this.statusCode < 5; } /** * @inheritDoc * * TODO: this is an ugly hack right now. * As soon as the value is an array, we assume it's multivalue * but REQUEST-STATUS is a (the one and only besides GEO) structured value and is also * stored inside an array. * * Calling icalProperty.setValues will throw an error */ toICALJs() { const icalProperty = createProperty(lc(this.name)); icalProperty.setValue(this.value); this._parameters.forEach((parameter) => { icalProperty.setParameter(lc(parameter.name), parameter.value); }); return icalProperty; } /** * Creates a new RequestStatusProperty from a code and a status message * * @param {number} code The status-code of the request * @param {string} message The message of the request * @return {RequestStatusProperty} */ static fromCodeAndMessage(code, message) { return new RequestStatusProperty("REQUEST-STATUS", [code.toString(), message]); } } RequestStatusProperty.SUCCESS = [2, "Success"]; RequestStatusProperty.SUCCESS_FALLBACK = [2.1, "Success, but fallback taken on one or more property values."]; RequestStatusProperty.SUCCESS_PROP_IGNORED = [2.2, "Success; invalid property ignored."]; RequestStatusProperty.SUCCESS_PROPPARAM_IGNORED = [2.3, "Success; invalid property parameter ignored."]; RequestStatusProperty.SUCCESS_NONSTANDARD_PROP_IGNORED = [2.4, "Success; unknown, non-standard property ignored."]; RequestStatusProperty.SUCCESS_NONSTANDARD_PROPPARAM_IGNORED = [2.5, "Success; unknown, non-standard property value ignored."]; RequestStatusProperty.SUCCESS_COMP_IGNORED = [2.6, "Success; invalid calendar component ignored."]; RequestStatusProperty.SUCCESS_FORWARDED = [2.7, "Success; request forwarded to Calendar User."]; RequestStatusProperty.SUCCESS_REPEATING_IGNORED = [2.8, "Success; repeating event ignored. Scheduled as a single component."]; RequestStatusProperty.SUCCESS_TRUNCATED_END = [2.9, "Success; truncated end date time to date boundary."]; RequestStatusProperty.SUCCESS_REPEATING_VTODO_IGNORED = [2.1, "Success; repeating VTODO ignored. Scheduled as a single VTODO."]; RequestStatusProperty.SUCCESS_UNBOUND_RRULE_CLIPPED = [2.11, "Success; unbounded RRULE clipped at some finite number of instances."]; RequestStatusProperty.CLIENT_INVALID_PROPNAME = [3, "Invalid property name."]; RequestStatusProperty.CLIENT_INVALID_PROPVALUE = [3.1, "Invalid property value."]; RequestStatusProperty.CLIENT_INVALID_PROPPARAM = [3.2, "Invalid property parameter."]; RequestStatusProperty.CLIENT_INVALID_PROPPARAMVALUE = [3.3, "Invalid property parameter value."]; RequestStatusProperty.CLIENT_INVALUD_CALENDAR_COMP_SEQ = [3.4, "Invalid calendar component sequence."]; RequestStatusProperty.CLIENT_INVALID_DATE_TIME = [3.5, "Invalid date or time."]; RequestStatusProperty.CLIENT_INVALID_RRULE = [3.6, "Invalid rule."]; RequestStatusProperty.CLIENT_INVALID_CU = [3.7, "Invalid Calendar User."]; RequestStatusProperty.CLIENT_NO_AUTHORITY = [3.8, "No authority."]; RequestStatusProperty.CLIENT_UNSUPPORTED_VERSION = [3.9, "Unsupported version."]; RequestStatusProperty.CLIENT_TOO_LARGE = [3.1, "Request entity too large."]; RequestStatusProperty.CLIENT_REQUIRED_COMP_OR_PROP_MISSING = [3.11, "Required component or property missing."]; RequestStatusProperty.CLIENT_UNKNOWN_COMP_OR_PROP = [3.12, "Unknown component or property found."]; RequestStatusProperty.CLIENT_UNSUPPORTED_COMP_OR_PROP = [3.13, "Unsupported component or property found."]; RequestStatusProperty.CLIENT_UNSUPPORTED_CAPABILITY = [3.14, "Unsupported capability."]; RequestStatusProperty.SCHEDULING_EVENT_CONFLICT = [4, "Event conflict. Date/time is busy."]; RequestStatusProperty.SERVER_REQUEST_NOT_SUPPORTED = [5, "Request not supported."]; RequestStatusProperty.SERVER_SERVICE_UNAVAILABLE = [5.1, "Service unavailable."]; RequestStatusProperty.SERVER_INVALID_CALENDAR_SERVICE = [5.2, "Invalid calendar service."]; RequestStatusProperty.SERVER_NO_SCHEDULING_FOR_USER = [5.3, "No scheduling support for user."]; /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class TextProperty extends Property { /** * Gets the alternate text * * @return {string} */ get alternateText() { return this.getParameterFirstValue("ALTREP"); } /** * Sets the alternate text * * @param {string} altRep The alternative text */ set alternateText(altRep) { this.updateParameterIfExist("ALTREP", altRep); } /** * Gets language of this property * * @return {string} */ get language() { return this.getParameterFirstValue("LANGUAGE"); } /** * Sets language of this property * * @param {string} language The language of the text */ set language(language) { this.updateParameterIfExist("LANGUAGE", language); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class TriggerProperty extends Property { /** * Gets the related parameter * * @return {string} */ get related() { if (!this.hasParameter("RELATED")) { return "START"; } return this.getParameterFirstValue("RELATED"); } /** * Sets the related parameter * * @param {string} related Either START or END */ set related(related) { this.updateParameterIfExist("RELATED", related); } /** * Gets the value of this trigger * (If you override the setter, you also have to override the getter or * it will simply be undefined) * * @return {string | number | AbstractValue | string[] | number[] | AbstractValue[]} */ get value() { return super.value; } /** * Set the value of this trigger * * @param {DurationValue|DateTimeValue} value The time of trigger */ set value(value) { super.value = value; if (value instanceof DateTimeValue) { this.deleteParameter("RELATED"); super.value = value.getInUTC(); } } /** * Gets whether this alarm trigger is relative * * @return {boolean} */ isRelative() { return this.getFirstValue() instanceof DurationValue; } /** * Creates a new absolute trigger * * @param {DateTimeValue} alarmTime Time to create Trigger from * @return {TriggerProperty} */ static fromAbsolute(alarmTime) { return new TriggerProperty("TRIGGER", alarmTime); } /** * Creates a new relative trigger * * @param {DurationValue} alarmOffset Duration to create Trigger from * @param {boolean=} relatedToStart Related to Start or end? * @return {TriggerProperty} */ static fromRelativeAndRelated(alarmOffset, relatedToStart = true) { return new TriggerProperty("TRIGGER", alarmOffset, [["RELATED", relatedToStart ? "START" : "END"]]); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @author Richard Steinmetz * * @license AGPL-3.0-or-later * * 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 . * */ function getConstructorForPropertyName(propName) { switch (uc(propName)) { case "ATTACH": return AttachmentProperty; case "ATTENDEE": case "ORGANIZER": return AttendeeProperty; case "CONFERENCE": return ConferenceProperty; case "FREEBUSY": return FreeBusyProperty; case "GEO": return GeoProperty; case "IMAGE": return ImageProperty; case "RELATED-TO": return RelationProperty; case "REQUEST-STATUS": return RequestStatusProperty; case "TRIGGER": return TriggerProperty; case "COMMENT": case "CONTACT": case "DESCRIPTION": case "LOCATION": case "SUMMARY": return TextProperty; default: return Property; } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class AbstractComponent extends observerTrait(lockableTrait(class { })) { /** * Constructor * * @param {string} name - Name of component * @param {Property[]} properties - Array of properties stored inside the component * @param {AbstractComponent[]} components - Array of subcomponents stored inside this component * @param {CalendarComponent|null} root - The root of this calendar document * @param {AbstractComponent|null} parent - The parent component of this element */ constructor(name, properties = [], components = [], root = null, parent = null) { super(); this._name = uc(name); this._properties = /* @__PURE__ */ new Map(); this._components = /* @__PURE__ */ new Map(); this._root = root; this._parent = parent; this._setPropertiesFromConstructor(properties); this._setComponentsFromConstructor(components); } /** * Get the component's name * * @return {string} */ get name() { return this._name; } /** * Gets the root of this calendar-document * * @return {CalendarComponent} */ get root() { return this._root; } /** * Sets the root of this calendar-document * * @param {CalendarComponent} root The new root element */ set root(root) { this._modify(); this._root = root; for (const property of this.getPropertyIterator()) { property.root = root; } for (const component of this.getComponentIterator()) { component.root = root; } } /** * Gets the parent component * * @return {AbstractComponent} */ get parent() { return this._parent; } /** * Sets the parent component * * @param {AbstractComponent} parent The new parent element */ set parent(parent) { this._modify(); this._parent = parent; } /** * Gets the first property that matches the given propertyName * * @param {string} propertyName Name of the property to get * @return {Property|null} */ getFirstProperty(propertyName) { if (!this._properties.has(uc(propertyName))) { return null; } return this._properties.get(uc(propertyName))[0]; } /** * Gets the first value of the first property matching that name * * @param {string} propertyName Name of the property to get first value of * @return {string | number | AbstractValue | string[] | number[] | AbstractValue[] | null} */ getFirstPropertyFirstValue(propertyName) { const property = this.getFirstProperty(propertyName); if (!property) { return null; } return property.getFirstValue(); } /** * update a property if it exists, * create a new one if it doesn't * * @param {string} propertyName Name of the property to update / create * @param {string | number | AbstractValue | string[] | number[] | AbstractValue[] | null} value The value to set */ updatePropertyWithValue(propertyName, value) { this._modify(); const property = this.getFirstProperty(propertyName); if (property) { property.value = value; } else { const constructor = getConstructorForPropertyName(propertyName); const newProperty = new constructor(propertyName, value, [], this, this.root); this.addProperty(newProperty); } } /** * Returns iterator for all properties of a given propertyName * or if no propertyName was given over all available properties * * @param {string=} propertyName Name of the property to get an iterator for */ *getPropertyIterator(propertyName = null) { if (propertyName) { if (!this.hasProperty(propertyName)) { return; } yield* this._properties.get(uc(propertyName)).slice()[Symbol.iterator](); } else { for (const key of this._properties.keys()) { yield* this.getPropertyIterator(key); } } } /** * Get all properties by name that match the given LANG parameter * * @param {string} propertyName The name of the property * @param {string | null} lang The lang to query * @private */ *_getAllOfPropertyByLang(propertyName, lang) { for (const property of this.getPropertyIterator(propertyName)) { if (property.getParameterFirstValue("LANGUAGE") === lang) { yield property; } } } /** * Get the first property by name that matches the given LANG parameter * * @param {string} propertyName The name of the property * @param {string | null} lang The lang to query * @return {Property|null} * @private */ _getFirstOfPropertyByLang(propertyName, lang) { const iterator = this._getAllOfPropertyByLang(propertyName, lang); return iterator.next().value || null; } /** * Adds a property * * @param {Property} property The property to add * @return {boolean} */ addProperty(property) { this._modify(); property.root = this.root; property.parent = this; if (this._properties.has(property.name)) { const arr = this._properties.get(property.name); if (arr.indexOf(property) !== -1) { return false; } arr.push(property); } else { this._properties.set(property.name, [property]); } property.subscribe(() => this._notifySubscribers()); return true; } /** * Checks if this component has a property of the given name * * @param {string} propertyName The name of the property * @return {boolean} */ hasProperty(propertyName) { return this._properties.has(uc(propertyName)); } /** * Removes the given property from this component * * @param {Property} property The property to delete * @return {boolean} */ deleteProperty(property) { this._modify(); if (!this._properties.has(property.name)) { return false; } const arr = this._properties.get(property.name); const index = arr.indexOf(property); if (index === -1) { return false; } if (index !== -1 && arr.length === 1) { this._properties.delete(property.name); } else { arr.splice(index, 1); } return true; } /** * Removes all properties of a given name * * @param {string} propertyName The name of the property * @return {boolean} */ deleteAllProperties(propertyName) { this._modify(); return this._properties.delete(uc(propertyName)); } /** * Gets the first component of a given name * * @param {string} componentName The name of the component * @return {AbstractComponent|null} */ getFirstComponent(componentName) { if (!this.hasComponent(componentName)) { return null; } return this._components.get(uc(componentName))[0]; } /** * Returns iterator for all components of a given componentName * or if no componentName was given over all available components * * @param {string=} componentName The name of the component */ *getComponentIterator(componentName) { if (componentName) { if (!this.hasComponent(componentName)) { return; } yield* this._components.get(uc(componentName)).slice()[Symbol.iterator](); } else { for (const key of this._components.keys()) { yield* this.getComponentIterator(key); } } } /** * Adds a new component to this component * * @param {AbstractComponent} component The component to add * @return {boolean} */ addComponent(component) { this._modify(); component.root = this.root; component.parent = this; if (this._components.has(component.name)) { const arr = this._components.get(component.name); if (arr.indexOf(component) !== -1) { return false; } arr.push(component); } else { this._components.set(component.name, [component]); } component.subscribe(() => this._notifySubscribers()); return true; } /** * Checks if this component has a component of the given name * * @param {string} componentName The name of the component * @return {boolean} */ hasComponent(componentName) { return this._components.has(uc(componentName)); } /** * Removes the given component from this component * * @param {AbstractComponent} component The component to delete * @return {boolean} */ deleteComponent(component) { this._modify(); if (!this._components.has(component.name)) { return false; } const arr = this._components.get(component.name); const index = arr.indexOf(component); if (index === -1) { return false; } if (index !== -1 && arr.length === 1) { this._components.delete(component.name); } else { arr.splice(index, 1); } return true; } /** * Removes all components of a given name * * @param {string} componentName The name of the component * @return {boolean} */ deleteAllComponents(componentName) { this._modify(); return this._components.delete(uc(componentName)); } /** * Marks this parameter is immutable * locks it against further modification */ lock() { super.lock(); for (const property of this.getPropertyIterator()) { property.lock(); } for (const component of this.getComponentIterator()) { component.lock(); } } /** * Marks this parameter as mutable * allowing further modification */ unlock() { super.unlock(); for (const property of this.getPropertyIterator()) { property.unlock(); } for (const component of this.getComponentIterator()) { component.unlock(); } } /** * Creates a copy of this parameter * * @return {AbstractComponent} */ clone() { const properties = []; for (const property of this.getPropertyIterator()) { properties.push(property.clone()); } const components = []; for (const component of this.getComponentIterator()) { components.push(component.clone()); } return new this.constructor(this.name, properties, components, this.root, this.parent); } /** * Adds properties from constructor to this._properties * * @param {Property[]} properties Array of properties * @private */ _setPropertiesFromConstructor(properties) { for (let property of properties) { if (Array.isArray(property)) { const constructor = getConstructorForPropertyName(property[0]); property = new constructor(property[0], property[1]); } this.addProperty(property); } } /** * Adds components from constructor to this._components * * @param {AbstractComponent[]} components Array of components * @private */ _setComponentsFromConstructor(components) { for (const component of components) { this.addComponent(component); } } /** * Creates a new Component based on an ical object * * @param {ICAL.Component} icalValue The ical.js component to initialise from * @param {CalendarComponent=} root The root of the Calendar Document * @param {AbstractComponent=} parent The parent element of this component * @return {AbstractComponent} */ static fromICALJs(icalValue, root = null, parent = null) { if (!(icalValue instanceof ICAL__default.default.Component)) { throw new ExpectedICalJSError(); } const name = icalValue.name; const newComponent = new this(name, [], [], root, parent); for (const icalProp of icalValue.getAllProperties()) { const constructor = getConstructorForPropertyName(icalProp.name); const property = constructor.fromICALJs(icalProp, root, newComponent); newComponent.addProperty(property); } for (const icalComp of icalValue.getAllSubcomponents()) { const constructor = this._getConstructorForComponentName(icalComp.name); const component = constructor.fromICALJs(icalComp, root, newComponent); newComponent.addComponent(component); } return newComponent; } /** * Gets a constructor for a give component name * * @param {string} componentName The name of the component * @return {AbstractComponent} * @protected */ static _getConstructorForComponentName(componentName) { return AbstractComponent; } /** * turns this Component into an ICAL.js component * * @return {ICAL.Component} */ toICALJs() { const component = createComponent(lc(this.name)); for (const prop of this.getPropertyIterator()) { component.addProperty(prop.toICALJs()); } for (const comp of this.getComponentIterator()) { component.addSubcomponent(comp.toICALJs()); } return component; } } function advertiseSingleOccurrenceProperty(prototype, options, advertiseValueOnly = true) { options = getDefaultOncePropConfig(options); Object.defineProperty(prototype, options.name, { get() { const value = this.getFirstPropertyFirstValue(options.iCalendarName); if (!value) { return options.defaultValue; } else { if (Array.isArray(options.allowedValues) && !options.allowedValues.includes(value)) { return options.unknownValue; } return value; } }, set(value) { this._modify(); if (value === null) { this.deleteAllProperties(options.iCalendarName); return; } if (Array.isArray(options.allowedValues) && !options.allowedValues.includes(value)) { throw new TypeError("Illegal value"); } this.updatePropertyWithValue(options.iCalendarName, value); } }); } function advertiseMultipleOccurrenceProperty(prototype, options) { options = getDefaultMultiplePropConfig(options); prototype["get" + ucFirst(options.name) + "Iterator"] = function* () { yield* this.getPropertyIterator(options.iCalendarName); }; prototype["get" + ucFirst(options.name) + "List"] = function() { return Array.from(this["get" + ucFirst(options.name) + "Iterator"]()); }; prototype["remove" + ucFirst(options.name)] = function(property) { this.deleteProperty(property); }; prototype["clearAll" + ucFirst(options.pluralName)] = function() { this.deleteAllProperties(options.iCalendarName); }; } function advertiseMultiValueStringPropertySeparatedByLang(prototype, options) { options = getDefaultMultiplePropConfig(options); prototype["get" + ucFirst(options.name) + "Iterator"] = function* (lang = null) { for (const property of this._getAllOfPropertyByLang(options.iCalendarName, lang)) { yield* property.getValueIterator(); } }; prototype["get" + ucFirst(options.name) + "List"] = function(lang = null) { return Array.from(this["get" + ucFirst(options.name) + "Iterator"](lang)); }; prototype["add" + ucFirst(options.name)] = function(value, lang = null) { const property = this._getFirstOfPropertyByLang(options.iCalendarName, lang); if (property) { property.addValue(value); } else { const newProperty = new Property(options.iCalendarName, [value]); if (lang) { const languageParameter = new Parameter("LANGUAGE", lang); newProperty.setParameter(languageParameter); } this.addProperty(newProperty); } }; prototype["remove" + ucFirst(options.name)] = function(value, lang = null) { for (const property of this._getAllOfPropertyByLang(options.iCalendarName, lang)) { if (property.isMultiValue() && property.hasValue(value)) { if (property.value.length === 1) { this.deleteProperty(property); return true; } property.removeValue(value); return true; } } return false; }; prototype["clearAll" + ucFirst(options.pluralName)] = function(lang = null) { for (const property of this._getAllOfPropertyByLang(options.iCalendarName, lang)) { this.deleteProperty(property); } }; } function advertiseComponent(prototype, options) { options = getDefaultMultipleCompConfig(options); prototype["get" + ucFirst(options.name) + "Iterator"] = function* () { yield* this.getComponentIterator(options.iCalendarName); }; prototype["get" + ucFirst(options.name) + "List"] = function() { return Array.from(this["get" + ucFirst(options.name) + "Iterator"]()); }; prototype["remove" + ucFirst(options.name)] = function(component) { this.deleteComponent(component); }; prototype["clearAll" + ucFirst(options.pluralName)] = function() { this.deleteAllComponents(options.iCalendarName); }; } function getDefaultOncePropConfig(options) { if (typeof options === "string") { options = { name: options }; } return Object.assign({}, { iCalendarName: uc(options.name), pluralName: options.name + "s", allowedValues: null, defaultValue: null, unknownValue: null }, options); } function getDefaultMultiplePropConfig(options) { if (typeof options === "string") { options = { name: options }; } return Object.assign({}, { iCalendarName: uc(options.name), pluralName: options.name + "s" }, options); } function getDefaultMultipleCompConfig(options) { if (typeof options === "string") { options = { name: options }; } return Object.assign({}, { iCalendarName: "V" + uc(options.name), pluralName: options.name + "s" }, options); } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ function dateFactory() { return /* @__PURE__ */ new Date(); } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class RecurringWithoutDtStartError extends Error { } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class RecurrenceManager { /** * Constructor * * @param {AbstractRecurringComponent} masterItem The master-item of the recurrence-set */ constructor(masterItem) { this._masterItem = masterItem; this._recurrenceExceptionItems = /* @__PURE__ */ new Map(); this._rangeRecurrenceExceptionItemsIndex = []; this._rangeRecurrenceExceptionItemsDiffCache = /* @__PURE__ */ new Map(); this._rangeRecurrenceExceptionItems = /* @__PURE__ */ new Map(); } /** * * @return {AbstractRecurringComponent} */ get masterItem() { return this._masterItem; } /** * * @param {AbstractRecurringComponent} masterItem The master-item of the recurrence-set */ set masterItem(masterItem) { this._masterItem = masterItem; } /** * Gets an iterator over all registered recurrence exceptions of this calendar-document */ *getRecurrenceExceptionIterator() { yield* this._recurrenceExceptionItems.values(); } /** * Gets a list of all registered recurrence-exceptions of this calendar-document * * @return {AbstractRecurringComponent[]} */ getRecurrenceExceptionList() { return Array.from(this.getRecurrenceExceptionIterator()); } /** * Checks if there is a recurrence Exception for a given recurrenceId * * @param {DateTimeValue | number} recurrenceId The recurrenceId to check * @return {boolean} */ hasRecurrenceExceptionForId(recurrenceId) { if (recurrenceId instanceof DateTimeValue) { recurrenceId = recurrenceId.unixTime; } else if (recurrenceId instanceof ICAL__default.default.Time) { recurrenceId = recurrenceId.toUnixTime(); } return this._recurrenceExceptionItems.has(recurrenceId); } /** * Gets the recurrence exception for a given recurrence Id * * @param {DateTimeValue | number} recurrenceId The recurrenceId to get * @return {AbstractRecurringComponent|null} */ getRecurrenceException(recurrenceId) { if (recurrenceId instanceof DateTimeValue) { recurrenceId = recurrenceId.unixTime; } else if (recurrenceId instanceof ICAL__default.default.Time) { recurrenceId = recurrenceId.toUnixTime(); } return this._recurrenceExceptionItems.get(recurrenceId) || null; } /** * Check if there is a recurrence-exception with a range for a given recurrence-id * * @param {DateTimeValue | number} recurrenceId The recurrenceId to check * @return {boolean} */ hasRangeRecurrenceExceptionForId(recurrenceId) { if (recurrenceId instanceof DateTimeValue) { recurrenceId = recurrenceId.unixTime; } else if (recurrenceId instanceof ICAL__default.default.Time) { recurrenceId = recurrenceId.toUnixTime(); } if (this._rangeRecurrenceExceptionItemsIndex.length === 0) { return false; } return this._rangeRecurrenceExceptionItemsIndex[0] < recurrenceId; } /** * Get recurrence-exception with range that's affecting the given recurrence-id * * @param {DateTimeValue | number} recurrenceId The recurrenceId to get * @return {AbstractRecurringComponent|null} */ getRangeRecurrenceExceptionForId(recurrenceId) { if (recurrenceId instanceof DateTimeValue) { recurrenceId = recurrenceId.unixTime; } else if (recurrenceId instanceof ICAL__default.default.Time) { recurrenceId = recurrenceId.toUnixTime(); } const index = ICAL__default.default.helpers.binsearchInsert( this._rangeRecurrenceExceptionItemsIndex, recurrenceId, (a, b) => a - b ); if (index === 0) { return null; } const key = this._rangeRecurrenceExceptionItemsIndex[index - 1]; return this._rangeRecurrenceExceptionItems.get(key); } /** * Gets the difference between recurrence-id and start * Mostly needed to handle recurrence-exceptions with range * * @param {DateTimeValue | number} recurrenceId The recurrenceId to get * @return {DurationValue|null} */ getRangeRecurrenceExceptionDiff(recurrenceId) { if (recurrenceId instanceof DateTimeValue) { recurrenceId = recurrenceId.unixTime; } else if (recurrenceId instanceof ICAL__default.default.Time) { recurrenceId = recurrenceId.toUnixTime(); } if (this._rangeRecurrenceExceptionItemsDiffCache.has(recurrenceId)) { return this._rangeRecurrenceExceptionItemsDiffCache.get(recurrenceId); } const recurrenceException = this.getRangeRecurrenceExceptionForId(recurrenceId); if (!recurrenceException) { return null; } const originalRecurrenceId = recurrenceException.recurrenceId; const originalModifiedStart = recurrenceException.startDate; const difference = originalModifiedStart.subtractDateWithTimezone(originalRecurrenceId); difference.lock(); this._rangeRecurrenceExceptionItemsDiffCache.set(recurrenceId, difference); return difference; } /** * Adds a new recurrence-exception to this calendar-document * * @param {AbstractRecurringComponent} recurrenceExceptionItem The recurrence-exception-item to relate to recurrence-set */ relateRecurrenceException(recurrenceExceptionItem) { this._modify(); const key = this._getRecurrenceIdKey(recurrenceExceptionItem); this._recurrenceExceptionItems.set(key, recurrenceExceptionItem); if (recurrenceExceptionItem.modifiesFuture()) { this._rangeRecurrenceExceptionItems.set(key, recurrenceExceptionItem); const index = ICAL__default.default.helpers.binsearchInsert( this._rangeRecurrenceExceptionItemsIndex, key, (a, b) => a - b ); this._rangeRecurrenceExceptionItemsIndex.splice(index, 0, key); } recurrenceExceptionItem.recurrenceManager = this; } /** * Removes a recurrence exception by the item itself * * @param {AbstractRecurringComponent} recurrenceExceptionItem The recurrence-exception remove */ removeRecurrenceException(recurrenceExceptionItem) { const key = this._getRecurrenceIdKey(recurrenceExceptionItem); this.removeRecurrenceExceptionByRecurrenceId(key); } /** * Removes a recurrence exception by it's unix-time * * @param {number} recurrenceId The recurrence-exception to remove */ removeRecurrenceExceptionByRecurrenceId(recurrenceId) { this._modify(); this._recurrenceExceptionItems.delete(recurrenceId); this._rangeRecurrenceExceptionItems.delete(recurrenceId); this._rangeRecurrenceExceptionItemsDiffCache.delete(recurrenceId); const index = this._rangeRecurrenceExceptionItemsIndex.indexOf(recurrenceId); if (index !== -1) { this._rangeRecurrenceExceptionItemsIndex.splice(index, 1); } } /** * * @param {AbstractRecurringComponent} recurrenceExceptionItem Object to get key from * @return {number} * @private */ _getRecurrenceIdKey(recurrenceExceptionItem) { return recurrenceExceptionItem.recurrenceId.unixTime; } /** * Gets an iterator over all recurrence rules */ *getRecurrenceRuleIterator() { for (const property of this._masterItem.getPropertyIterator("RRULE")) { yield property.getFirstValue(); } } /** * Gets a list of all recurrence rules * * @return {RecurValue[]} */ getRecurrenceRuleList() { return Array.from(this.getRecurrenceRuleIterator()); } /** * Adds a new recurrence rule * * @param {RecurValue} recurrenceRule The RRULE to add */ addRecurrenceRule(recurrenceRule) { this._modify(); this.resetCache(); const property = new Property("RRULE", recurrenceRule); this._masterItem.addProperty(property); } /** * Removes a recurrence rule * * @param {RecurValue} recurrenceRule The RRULE to remove */ removeRecurrenceRule(recurrenceRule) { this._modify(); this.resetCache(); for (const property of this._masterItem.getPropertyIterator("RRULE")) { if (property.getFirstValue() === recurrenceRule) { this._masterItem.deleteProperty(property); } } } /** * Removes all recurrence rules */ clearAllRecurrenceRules() { this._modify(); this.resetCache(); this._masterItem.deleteAllProperties("RRULE"); } /** * Gets an iterator over all recurrence * * @param {boolean} isNegative Whether or not to get EXDATES * @param {string} valueType Limit type of EXDATES */ *getRecurrenceDateIterator(isNegative = false, valueType = null) { for (const property of this._getPropertiesForRecurrenceDate(isNegative, valueType)) { yield* property.getValueIterator(); } } /** * * @param {boolean} isNegative Whether or not to get EXDATES * @param {string} valueType Limit type of EXDATES * @return {(DateTimeValue|PeriodValue)[]} */ listAllRecurrenceDates(isNegative = false, valueType = null) { return Array.from(this.getRecurrenceDateIterator(isNegative, valueType)); } /** * This adds a new recurrence-date value. * It automatically adds it to the first property of the same value-type * or creates a new one if necessary * * @param {boolean} isNegative Whether we are dealing with an EXDATE or RDATE * @param {DateTimeValue|PeriodValue} value EXDATE to add */ addRecurrenceDate(isNegative = false, value) { this._modify(); this.resetCache(); let timezoneId = null; if (value instanceof DateTimeValue && !value.isDate) { timezoneId = value.timezoneId; } const valueType = this._getValueTypeByValue(value); const iterator = this._getPropertiesForRecurrenceDate(isNegative, valueType, timezoneId); const first = iterator.next.value; if (first instanceof Property) { const propertyValue = first.value; propertyValue.push(value); this.masterItem.markPropertyAsDirty(isNegative ? "EXDATE" : "RDATE"); } else { const propertyName = this._getPropertyNameByIsNegative(isNegative); const property = new Property(propertyName, value); this._masterItem.addProperty(property); } } /** * Checks if a recurrenceID is an RDATE or EXDATE * * @param {boolean} isNegative Whether we are dealing with an EXDATE or RDATE * @param {DateTimeValue} recurrenceId Recurrence-Id to check * @return {boolean} */ hasRecurrenceDate(isNegative = false, recurrenceId) { for (let value of this.getRecurrenceDateIterator(isNegative)) { if (value instanceof PeriodValue) { value = value.start; } if (value.compare(recurrenceId) === 0) { return true; } } return false; } /** * * @param {boolean} isNegative Whether we are dealing with an EXDATE or RDATE * @param {DateTimeValue} recurrenceId Recurrence-Id to get * @return {null|DateTimeValue|PeriodValue} */ getRecurrenceDate(isNegative = false, recurrenceId) { for (const value of this.getRecurrenceDateIterator(isNegative)) { let valueToCheck = value; if (valueToCheck instanceof PeriodValue) { valueToCheck = valueToCheck.start; } if (valueToCheck.compare(recurrenceId) === 0) { return value; } } return null; } /** * This deletes a recurrence-date value from this recurrence-set * * @param {boolean} isNegative Whether we are dealing with an EXDATE or RDATE * @param {DateTimeValue|PeriodValue} value The EXDATE/RDATE to remove */ removeRecurrenceDate(isNegative = false, value) { this._modify(); this.resetCache(); const valueType = this._getValueTypeByValue(value); for (const property of this._getPropertiesForRecurrenceDate(isNegative, valueType)) { for (const valueToCheck of property.getValueIterator()) { if (value === valueToCheck) { const allValues = property.value; if (allValues.length === 1) { this.masterItem.deleteProperty(property); continue; } const index = allValues.indexOf(value); allValues.splice(index, 1); this.masterItem.markPropertyAsDirty(isNegative ? "EXDATE" : "RDATE"); } } } } /** * Clears all recurrence-date information * * @param {boolean} isNegative Whether we are dealing with an EXDATE or RDATE * @param {string} valueType The type of RDATEs/EXDATEs to remove */ clearAllRecurrenceDates(isNegative = false, valueType = null) { this._modify(); this.resetCache(); for (const property of this._getPropertiesForRecurrenceDate(isNegative, valueType)) { this._masterItem.deleteProperty(property); } } /** * Gets the property name for recurrence dates based on the isNegative boolean * * @param {boolean} isNegative Whether we are dealing with an EXDATE or RDATE * @return {string} * @private */ _getPropertyNameByIsNegative(isNegative) { return isNegative ? "EXDATE" : "RDATE"; } /** * Gets the value type based on the provided value * * @param {PeriodValue|DateTimeValue} value The value to get type of property from * @return {string} * @private */ _getValueTypeByValue(value) { if (value instanceof PeriodValue) { return "PERIOD"; } else if (value.isDate) { return "DATE"; } else { return "DATETIME"; } } /** * * @param {boolean} isNegative Whether we are dealing with an EXDATE or RDATE * @param {string | null} valueType The type of values to get * @param {ICAL.Timezone=} timezoneId Filter by timezone * @private */ *_getPropertiesForRecurrenceDate(isNegative, valueType, timezoneId = null) { const propertyName = this._getPropertyNameByIsNegative(isNegative); for (const property of this._masterItem.getPropertyIterator(propertyName)) { if (valueType === null) { yield property; } else if (uc(valueType) === "PERIOD" && property.getFirstValue() instanceof PeriodValue) { yield property; } else if (uc(valueType) === "DATE" && property.getFirstValue().isDate) { yield property; } else if (uc(valueType) === "DATETIME" && !property.getFirstValue().isDate) { if (timezoneId === null || property.getFirstValue().timezoneId === timezoneId) { yield property; } } } } /** * Checks if the entire set of recurrence rules is finite * * @return {boolean} */ isFinite() { return this.getRecurrenceRuleList().every((rule) => rule.isFinite()); } /** * @return {boolean} */ isEmptyRecurrenceSet() { return this._getRecurExpansionObject().next() === void 0; } /** * Gets the occurrence at the exact given recurrenceId * * @param {DateTimeValue} recurrenceId RecurrenceId to get * @return {AbstractRecurringComponent|null} */ getOccurrenceAtExactly(recurrenceId) { if (!this.masterItem.isRecurring()) { if (this.masterItem.getReferenceRecurrenceId().compare(recurrenceId) === 0) { return this.masterItem; } return null; } const iterator = this._getRecurExpansionObject(); const icalRecurrenceId = recurrenceId.toICALJs(); let next; while (next = iterator.next()) { if (next.compare(icalRecurrenceId) === 0) { return this._getOccurrenceAtRecurrenceId(DateTimeValue.fromICALJs(next)); } if (next.compare(icalRecurrenceId) === 1) { return null; } } return null; } /** * Gets the closest occurrence to the given recurrenceId. * That's either the closest in the future, or in case the * recurrence-set ends before recurrenceId, the last one * * This function works solely on the basis of recurrence-ids. * It ignores the actual date of recurrence-exceptions. * Ideally we should fix it and provide a similar implementation * like getAllOccurrencesBetweenIterator, but for now it's the * accepted behavior. * * @param {DateTimeValue} recurrenceId RecurrenceId to get * @return {AbstractRecurringComponent} */ getClosestOccurrence(recurrenceId) { if (!this.masterItem.isRecurring()) { return this.masterItem; } const iterator = this._getRecurExpansionObject(); recurrenceId = recurrenceId.toICALJs(); let previous = null; let next; while (next = iterator.next()) { if (next.compare(recurrenceId) === -1) { previous = next; } else { const dateTimeValue2 = DateTimeValue.fromICALJs(next); return this._getOccurrenceAtRecurrenceId(dateTimeValue2); } } const dateTimeValue = DateTimeValue.fromICALJs(previous); return this._getOccurrenceAtRecurrenceId(dateTimeValue); } /** * Counts all occurrences in the given time-range. * This function works solely on the basis of recurrence-ids. * Start and end are inclusive. * * @param {DateTimeValue} queriedTimeRangeStart Start of time-range * @param {DateTimeValue} queriedTimeRangeEnd End of time-range * @return {number} Count of occurrences in the given time-range */ countAllOccurrencesBetween(queriedTimeRangeStart, queriedTimeRangeEnd) { if (!this.masterItem.isRecurring()) { if (typeof this.masterItem.isInTimeFrame === "function" && !this.masterItem.isInTimeFrame(queriedTimeRangeStart, queriedTimeRangeEnd)) { return 0; } return 1; } const iterator = this._getRecurExpansionObject(); const queriedICALJsTimeRangeStart = queriedTimeRangeStart.toICALJs(); const queriedICALJsTimeRangeEnd = queriedTimeRangeEnd.toICALJs(); let count = 0; let next; while (next = iterator.next()) { if (next.compare(queriedICALJsTimeRangeStart) === -1) { continue; } if (next.compare(queriedICALJsTimeRangeEnd) === 1) { break; } count += 1; } return count; } /** * Get all occurrences between start and end * Start and End are inclusive * * @param {DateTimeValue} queriedTimeRangeStart Start of time-range * @param {DateTimeValue} queriedTimeRangeEnd End of time-range */ *getAllOccurrencesBetweenIterator(queriedTimeRangeStart, queriedTimeRangeEnd) { if (!this.masterItem.isRecurring()) { if (typeof this.masterItem.isInTimeFrame !== "function") { yield this.masterItem; } if (this.masterItem.isInTimeFrame(queriedTimeRangeStart, queriedTimeRangeEnd)) { yield this.masterItem; } return; } const iterator = this._getRecurExpansionObject(); const queriedICALJsTimeRangeStart = queriedTimeRangeStart.toICALJs(); const queriedICALJsTimeRangeEnd = queriedTimeRangeEnd.toICALJs(); const recurrenceIdKeys = Array.from(this._recurrenceExceptionItems.keys()); const maximumRecurrenceId = Math.max.apply(Math, recurrenceIdKeys); let next; while (next = iterator.next()) { const dateTimeValue = DateTimeValue.fromICALJs(next); const occurrence = this._getOccurrenceAtRecurrenceId(dateTimeValue); let compareDate = null; switch (uc(occurrence.name)) { case "VEVENT": case "VTODO": compareDate = occurrence.endDate.toICALJs(); break; case "VJOURNAL": default: compareDate = next; break; } if (compareDate.compare(queriedICALJsTimeRangeStart) === -1) { continue; } const startDate = occurrence.startDate.toICALJs(); if ((!occurrence.isRecurrenceException() || occurrence.modifiesFuture()) && startDate.compare(queriedICALJsTimeRangeEnd) === 1) { if (this._recurrenceExceptionItems.size === 0) { break; } if (next.toUnixTime() > maximumRecurrenceId) { break; } else { continue; } } if (typeof occurrence.isInTimeFrame !== "function") { yield occurrence; } if (occurrence.isInTimeFrame(queriedTimeRangeStart, queriedTimeRangeEnd)) { yield occurrence; } } } /** * Get all occurrences between start and end * * @param {DateTimeValue} start Start of time-range * @param {DateTimeValue} end End of time-range * @return {(*|null)[]} */ getAllOccurrencesBetween(start, end) { return Array.from(this.getAllOccurrencesBetweenIterator(start, end)); } /** * Update the UID of all components in the recurrence set * * @param {string} newUID The new UID of the calendar-document */ updateUID(newUID) { this._masterItem.updatePropertyWithValue("UID", newUID); for (const recurrenceExceptionItem of this.getRecurrenceExceptionIterator()) { recurrenceExceptionItem.updatePropertyWithValue("UID", newUID); } } /** * Updates the recurrence-information accordingly, * whenever the start-date of the master-item changes * * @param {DateTimeValue} newStartDate The new start-date * @param {DateTimeValue} oldStartDate The old start-date */ updateStartDateOfMasterItem(newStartDate, oldStartDate) { const difference = newStartDate.subtractDateWithTimezone(oldStartDate); for (const exdate of this.getRecurrenceDateIterator(true)) { if (this.hasRecurrenceDate(false, exdate)) { continue; } exdate.addDuration(difference); } for (const recurrenceException of this.getRecurrenceExceptionIterator()) { if (this.hasRecurrenceDate(false, recurrenceException.recurrenceId)) { continue; } this.removeRecurrenceException(recurrenceException); recurrenceException.recurrenceId.addDuration(difference); this.relateRecurrenceException(recurrenceException); } for (const rrule of this.getRecurrenceRuleIterator()) { if (rrule.until) { rrule.until.addDuration(difference); } } } /** * Gets an object for the given recurrenceId * It does not verify that the given recurrenceId * is actually a valid recurrence of this calendar-document * * @param {DateTimeValue} recurrenceId Recurrence-Id to get * @return {AbstractRecurringComponent} * @private */ _getOccurrenceAtRecurrenceId(recurrenceId) { if (this.hasRecurrenceExceptionForId(recurrenceId)) { const recurrenceException = this.getRecurrenceException(recurrenceId); if (!recurrenceException.canCreateRecurrenceExceptions()) { return recurrenceException; } return recurrenceException.forkItem(recurrenceId); } else if (this.hasRangeRecurrenceExceptionForId(recurrenceId)) { const rangeRecurrenceException = this.getRangeRecurrenceExceptionForId(recurrenceId); const difference = this.getRangeRecurrenceExceptionDiff(recurrenceId); return rangeRecurrenceException.forkItem(recurrenceId, difference); } else if (recurrenceId.compare(this._masterItem.startDate) === 0) { if (!this._masterItem.canCreateRecurrenceExceptions()) { return this._masterItem; } return this._masterItem.forkItem(recurrenceId); } else { return this._masterItem.forkItem(recurrenceId); } } /** * Resets the internal recur-expansion object. * This is necessary after each modification of the * recurrence-information */ resetCache() { } /** * Gets a new ICAL.RecurExpansion object * * Inspired by how ICAL.JS RecurExpansion * serialises and unserialises its state * * @return {ICAL.RecurExpansion} * @private */ _getRecurExpansionObject() { if (this._masterItem.startDate === null) { throw new RecurringWithoutDtStartError(); } const dtstart = this._masterItem.startDate.toICALJs(); let last = dtstart.clone(); const ruleIterators = []; let ruleDateInc; const ruleDates = []; let ruleDate = null; const exDates = []; const complete = false; for (const ruleValue of this.getRecurrenceRuleIterator()) { ruleIterators.push(ruleValue.toICALJs().iterator(dtstart)); ruleIterators[ruleIterators.length - 1].next(); } for (let rDateValue of this.getRecurrenceDateIterator()) { if (rDateValue instanceof PeriodValue) { rDateValue = rDateValue.start; } rDateValue = rDateValue.toICALJs(); const index = ICAL__default.default.helpers.binsearchInsert( ruleDates, rDateValue, (a, b) => a.compare(b) ); ruleDates.splice(index, 0, rDateValue); } if (ruleDates.length > 0 && ruleDates[0].compare(dtstart) === -1) { ruleDateInc = 0; last = ruleDates[0].clone(); } else { ruleDateInc = ICAL__default.default.helpers.binsearchInsert( ruleDates, dtstart, (a, b) => a.compare(b) ); ruleDate = exDates[ruleDateInc]; } for (let exDateValue of this.getRecurrenceDateIterator(true)) { exDateValue = exDateValue.toICALJs(); const index = ICAL__default.default.helpers.binsearchInsert( exDates, exDateValue, (a, b) => a.compare(b) ); exDates.splice(index, 0, exDateValue); } const exDateInc = ICAL__default.default.helpers.binsearchInsert( exDates, dtstart, (a, b) => a.compare(b) ); const exDate = exDates[exDateInc]; return new ICAL__default.default.RecurExpansion({ dtstart, last, ruleIterators, ruleDateInc, exDateInc, ruleDates, ruleDate, exDates, exDate, complete }); } /** * @private */ _modify() { if (this._masterItem.isLocked()) { throw new ModificationNotAllowedError(); } } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class AlarmComponent extends AbstractComponent { /** * Adds a new attendee based on their name and email-address * * @param {string} name - Name of the attendee * @param {string} email - E-Mail address of the attendee * @return {boolean} */ addAttendeeFromNameAndEMail(name, email) { const attendeeProperty = AttendeeProperty.fromNameAndEMail(name, email); return this.addProperty(attendeeProperty); } /** * Gets the trigger property * * @url https://tools.ietf.org/html/rfc5545#section-3.8.6.3 * * @return {TriggerProperty} */ get trigger() { return this.getFirstProperty("TRIGGER"); } /** * Sets an absolute alarm * * @param {DateTimeValue} alarmTime - Absolute time for the trigger */ setTriggerFromAbsolute(alarmTime) { const triggerProperty = TriggerProperty.fromAbsolute(alarmTime); this.deleteAllProperties("TRIGGER"); this.addProperty(triggerProperty); } /** * Sets a relative trigger * * @param {DurationValue} alarmOffset - Relative time of the trigger, either related to start or end * @param {boolean=} relatedToStart - Related to Start or end? */ setTriggerFromRelative(alarmOffset, relatedToStart = true) { const triggerProperty = TriggerProperty.fromRelativeAndRelated(alarmOffset, relatedToStart); this.deleteAllProperties("TRIGGER"); this.addProperty(triggerProperty); } } advertiseSingleOccurrenceProperty(AlarmComponent.prototype, "action"); advertiseSingleOccurrenceProperty(AlarmComponent.prototype, "description"); advertiseSingleOccurrenceProperty(AlarmComponent.prototype, "summary"); advertiseSingleOccurrenceProperty(AlarmComponent.prototype, "duration"); advertiseSingleOccurrenceProperty(AlarmComponent.prototype, "repeat"); advertiseSingleOccurrenceProperty(AlarmComponent.prototype, { name: "attachment", iCalendarName: "ATTACH" }); advertiseMultipleOccurrenceProperty(AlarmComponent.prototype, "attendee"); /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ function getConstructorForComponentName$1(compName) { switch (uc(compName)) { case "VALARM": return AlarmComponent; default: return AbstractComponent; } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class AbstractRecurringComponent extends AbstractComponent { /** * @inheritDoc */ constructor(...args) { super(...args); this._primaryItem = null; this._isExactForkOfPrimary = false; this._originalRecurrenceId = null; this._recurrenceManager = null; this._dirty = false; this._significantChange = false; this._cachedId = null; } /** * Gets the primary-item of this recurring item * * @return {AbstractRecurringComponent} */ get primaryItem() { return this._primaryItem; } /** * Sets the primary-item of this recurring item * * @param {AbstractRecurringComponent} primaryItem The new primary-item */ set primaryItem(primaryItem) { this._modify(); this._primaryItem = primaryItem; } /** * Gets whether or not this is a fork of the primary item * for the same recurrence-id * * @return {boolean} */ get isExactForkOfPrimary() { return this._isExactForkOfPrimary; } /** * Sets the isExactForkOfPrimary indicator, see getter for description * * @param {boolean} isExactForkOfPrimary Whether or not this is an exact fork */ set isExactForkOfPrimary(isExactForkOfPrimary) { this._isExactForkOfPrimary = isExactForkOfPrimary; } /** * Gets the original recurrence-id * * @return {DateTimeValue} */ get originalRecurrenceId() { return this._originalRecurrenceId; } /** * Sets the original recurrence-id * * @param {DateTimeValue} originalRecurrenceId The new original recurrence-id */ set originalRecurrenceId(originalRecurrenceId) { this._originalRecurrenceId = originalRecurrenceId; } /** * Gets the recurrence-manager of this recurrence-set * * @return {RecurrenceManager} */ get recurrenceManager() { return this._recurrenceManager; } /** * Sets the recurrence-manager of this recurrence-set * * @param {RecurrenceManager} recurrenceManager The new recurrence-manager */ set recurrenceManager(recurrenceManager) { this._recurrenceManager = recurrenceManager; } /** * Gets the master-item of this recurring item * * @return {AbstractRecurringComponent} */ get masterItem() { return this.recurrenceManager.masterItem; } /** * Returns whether this item is the master item * * @return {boolean} */ isMasterItem() { return this.masterItem === this; } /** * Gets a unique ID for this occurrence of the event * * Please note that if the same event occurs in multiple calendars, * this id will not be unique. Software using this library will have to * manually mix in the calendar id into this id * * @return {string} */ get id() { if (this._cachedId) { return this._cachedId; } if (this.startDate === null) { this._cachedId = encodeURIComponent(this.uid); return this._cachedId; } this._cachedId = [ encodeURIComponent(this.uid), encodeURIComponent(this.getReferenceRecurrenceId().unixTime.toString()) ].join("###"); return this._cachedId; } /** * Gets the UID property * * @return {string | null} */ get uid() { return this.getFirstPropertyFirstValue("UID"); } /** * Sets the UID property and the UID property of all related exceptions * * @param {string} uid The new UID */ set uid(uid) { this._recurrenceManager.updateUID(uid); } /** * Gets the start date of the event * * @return {DateTimeValue} */ get startDate() { return this.getFirstPropertyFirstValue("dtstart"); } /** * Sets the start date of the event * * @param {DateTimeValue} start The new start-date to set */ set startDate(start) { const oldStartDate = this.startDate; this.updatePropertyWithValue("dtstart", start); if (this.isMasterItem()) { this._recurrenceManager.updateStartDateOfMasterItem(start, oldStartDate); } } /** * Checks whether this item is part of a recurring set * * @return {boolean} */ isPartOfRecurrenceSet() { return this.masterItem.isRecurring(); } /** * Checks whether this component is recurring * * @return {boolean} */ isRecurring() { return this.hasProperty("RRULE") || this.hasProperty("RDATE"); } /** * Checks whether this component is a recurrence-exception * * @return {boolean} */ isRecurrenceException() { return this.hasProperty("RECURRENCE-ID"); } /** * Checks wether this component is a recurrence-exception * and whether it's modifying the future * * @return {boolean} */ modifiesFuture() { if (!this.isRecurrenceException()) { return false; } const property = this.getFirstProperty("RECURRENCE-ID"); return property.getParameterFirstValue("RANGE") === "THISANDFUTURE"; } /** * Creates an occurrence at the given time * * This is an internal function for calendar-js, used by the recurrence-manager * Do not call from outside * * @param {DateTimeValue} recurrenceId The recurrence-Id of the forked item * @param {DurationValue=} startDiff to be used when The start-diff (used for RECURRENCE-ID;RANGE=THISANDFUTURE) * @return {AbstractRecurringComponent} */ forkItem(recurrenceId, startDiff = null) { const occurrence = this.clone(); occurrence.recurrenceManager = this.recurrenceManager; occurrence.primaryItem = this; if (occurrence.getReferenceRecurrenceId().compare(recurrenceId) === 0) { occurrence.isExactForkOfPrimary = true; } if (!occurrence.hasProperty("DTSTART")) { throw new TypeError("Can't fork item without a DTSTART"); } const rrule = occurrence.getFirstPropertyFirstValue("RRULE"); if (rrule?.count) { let index = occurrence.recurrenceManager.countAllOccurrencesBetween( occurrence.getReferenceRecurrenceId(), recurrenceId ); index -= 1; rrule.count -= index; if (rrule.count < 1) { rrule.count = 1; } } if (occurrence.getFirstPropertyFirstValue("DTSTART").timezoneId !== recurrenceId.timezoneId) { const originalTimezone = occurrence.getFirstPropertyFirstValue("DTSTART").getICALTimezone(); recurrenceId = recurrenceId.getInICALTimezone(originalTimezone); } occurrence.originalRecurrenceId = recurrenceId.clone(); const dtStartValue = occurrence.getFirstPropertyFirstValue("DTSTART"); let period = null; if (this._recurrenceManager.hasRecurrenceDate(false, recurrenceId)) { const recurrenceDate = this._recurrenceManager.getRecurrenceDate(false, recurrenceId); if (recurrenceDate instanceof PeriodValue) { period = recurrenceDate; } } let duration; if (occurrence.hasProperty("DTEND")) { const dtEndValue = occurrence.getFirstPropertyFirstValue("DTEND"); duration = dtEndValue.subtractDateWithTimezone(dtStartValue); } else if (occurrence.hasProperty("DUE")) { const dueValue = occurrence.getFirstPropertyFirstValue("DUE"); duration = dueValue.subtractDateWithTimezone(dtStartValue); } if (!(occurrence.isRecurrenceException() && occurrence.isExactForkOfPrimary)) { occurrence.updatePropertyWithValue("DTSTART", recurrenceId.clone()); if (startDiff) { occurrence.startDate.addDuration(startDiff); } if (occurrence.hasProperty("DTEND")) { const dtEnd = occurrence.startDate.clone(); dtEnd.addDuration(duration); occurrence.updatePropertyWithValue("DTEND", dtEnd); } else if (occurrence.hasProperty("DUE")) { const due = occurrence.startDate.clone(); due.addDuration(duration); occurrence.updatePropertyWithValue("DUE", due); } if (period) { occurrence.deleteAllProperties("DTEND"); occurrence.deleteAllProperties("DURATION"); occurrence.updatePropertyWithValue("DTEND", period.end.clone()); } } occurrence.resetDirty(); return occurrence; } /** * Checks whether it's possible to create a recurrence exception for this event * It is possible * * @return {boolean} */ canCreateRecurrenceExceptions() { let primaryIsRecurring = false; if (this.primaryItem && this.primaryItem.isRecurring()) { primaryIsRecurring = true; } return this.isRecurring() || this.modifiesFuture() || !this.isRecurring() && primaryIsRecurring; } /** * creates a recurrence exception based on this event * If the parameter thisAndAllFuture is set to true, * it will apply changes to this and all future occurrences * * @param {boolean} thisAndAllFuture Whether to create an exception for this and all future * @return {AbstractRecurringComponent[]} the AbstractRecurringComponent of the future events. * In case you set `thisAndAllFuture` to true, this will be an * AbstractRecurringComponent inside a entirely new calendar component */ createRecurrenceException(thisAndAllFuture = false) { if (!this.canCreateRecurrenceExceptions()) { throw new Error("Can't create recurrence-exceptions for non-recurring items"); } const previousPrimaryItem = this.primaryItem; if (thisAndAllFuture) { if (this.isExactForkOfPrimary) { if (this.primaryItem.isMasterItem()) { this._overridePrimaryItem(); return [this, this]; } } this.removeThisOccurrence(true); this.recurrenceManager = new RecurrenceManager(this); this._originalRecurrenceId = null; this.primaryItem = this; this.updatePropertyWithValue("UID", uuid.v4()); this._cachedId = null; this.addRelation("SIBLING", previousPrimaryItem.uid); previousPrimaryItem.addRelation("SIBLING", this.uid); this.deleteAllProperties("RECURRENCE-ID"); this.deleteAllProperties("RDATE"); this.deleteAllProperties("EXDATE"); this.updatePropertyWithValue("CREATED", DateTimeValue.fromJSDate(dateFactory(), true)); this.updatePropertyWithValue("DTSTAMP", DateTimeValue.fromJSDate(dateFactory(), true)); this.updatePropertyWithValue("LAST-MODIFIED", DateTimeValue.fromJSDate(dateFactory(), true)); this.updatePropertyWithValue("SEQUENCE", 0); this._significantChange = false; this._dirty = false; this.root = this.root.constructor.fromEmpty(); this.root.addComponent(this); this.parent = this.root; for (const attendee of this.getAttendeeIterator()) { attendee.rsvp = true; } } else { this.deleteAllProperties("RECURRENCE-ID"); this.recurrenceId = this.getReferenceRecurrenceId().clone(); this.root.addComponent(this); this.recurrenceManager.relateRecurrenceException(this); this.primaryItem = this; this.deleteAllProperties("RDATE"); this.deleteAllProperties("RRULE"); this.deleteAllProperties("EXDATE"); this.updatePropertyWithValue("CREATED", DateTimeValue.fromJSDate(dateFactory(), true)); this.updatePropertyWithValue("DTSTAMP", DateTimeValue.fromJSDate(dateFactory(), true)); this.updatePropertyWithValue("LAST-MODIFIED", DateTimeValue.fromJSDate(dateFactory(), true)); this.updatePropertyWithValue("SEQUENCE", 0); if (this.recurrenceManager.hasRecurrenceDate(false, this.getReferenceRecurrenceId())) { const recurDate = this.recurrenceManager.getRecurrenceDate(false, this.getReferenceRecurrenceId()); if (recurDate instanceof PeriodValue) { const valueDateTimeRecurDate = recurDate.start; this.recurrenceManager.removeRecurrenceDate(false, recurDate); this.recurrenceManager.addRecurrenceDate(false, valueDateTimeRecurDate); } } this.originalRecurrenceId = null; } return [previousPrimaryItem, this]; } /** * Deletes this occurrence from the series of recurring events * If the parameter thisAndAllFuture is set to true, * it will remove this and all future occurrences * * @param {boolean} thisAndAllFuture Whether to create an exception for this and all future * @throws EmptyRecurrenceSetError Thrown, when deleting an occurrence results in no more events * @return {boolean} true if this deleted the last occurrence in set, false if there are occurrences left */ removeThisOccurrence(thisAndAllFuture = false) { if (!this.isPartOfRecurrenceSet()) { return true; } if (thisAndAllFuture) { const recurrenceId = this.getReferenceRecurrenceId().clone(); const until = recurrenceId.getInTimezone(timezones.Timezone.utc); until.addDuration(DurationValue.fromSeconds(-1)); for (const recurValue of this.recurrenceManager.getRecurrenceRuleIterator()) { recurValue.until = until.clone(); } for (const recurDate of this.recurrenceManager.getRecurrenceDateIterator()) { let valueToCheck = recurDate; if (recurDate instanceof PeriodValue) { valueToCheck = valueToCheck.start; } if (recurrenceId.compare(valueToCheck) <= 0) { this.recurrenceManager.removeRecurrenceDate(false, recurDate); } } for (const exceptionDate of this.recurrenceManager.getRecurrenceDateIterator(true)) { if (recurrenceId.compare(exceptionDate) <= 0) { this.recurrenceManager.removeRecurrenceDate(true, exceptionDate); } } for (const exception of this.recurrenceManager.getRecurrenceExceptionList()) { if (recurrenceId.compare(exception.recurrenceId) <= 0) { this.root.deleteComponent(exception); this.recurrenceManager.removeRecurrenceException(exception); } } } else { if (this.isRecurrenceException() && !this.modifiesFuture()) { this.root.deleteComponent(this); this.recurrenceManager.removeRecurrenceException(this); } if (this.recurrenceManager.hasRecurrenceDate(false, this.getReferenceRecurrenceId())) { const recurDate = this.recurrenceManager.getRecurrenceDate(false, this.getReferenceRecurrenceId()); this.recurrenceManager.removeRecurrenceDate(false, recurDate); } else { this.recurrenceManager.addRecurrenceDate(true, this.getReferenceRecurrenceId().clone()); } } return this.recurrenceManager.isEmptyRecurrenceSet(); } /** * @inheritDoc */ clone() { const comp = super.clone(); comp.resetDirty(); return comp; } /** * Adds a new attendee * * @param {AttendeeProperty} attendee The attendee property to add * @private * @return {boolean} */ _addAttendee(attendee) { for (const a of this.getAttendeeIterator()) { if (a.email === attendee.email) { return false; } } this.addProperty(attendee); return true; } /** * Adds a new attendee based on their name and email-address * * @param {string} name The name of the attendee to add * @param {string} email The email-address of the attendee to add * @return {boolean} */ addAttendeeFromNameAndEMail(name, email) { const attendeeProperty = AttendeeProperty.fromNameAndEMail(name, email); return this._addAttendee(attendeeProperty); } /** * Adds a new attendee based on their properties * * @param {string} name The name of the attendee to add * @param {string} email The email-address of the attendee to add * @param {string} role The role of the attendee to add * @param {string} userType The type of attendee to add * @param {boolean} rsvp Whether or not to request a response from the attendee * @return {boolean} */ addAttendeeFromNameEMailRoleUserTypeAndRSVP(name, email, role, userType, rsvp) { const attendeeProperty = AttendeeProperty.fromNameEMailRoleUserTypeAndRSVP(name, email, role, userType, rsvp, false); return this._addAttendee(attendeeProperty); } /** * Sets the organiser property from common-name and email address * * @param {string} name The name of the organizer * @param {string} email The email-address of the organizer */ setOrganizerFromNameAndEMail(name, email) { this.deleteAllProperties("ORGANIZER"); this.addProperty(AttendeeProperty.fromNameAndEMail(name, email, true)); } /** * Adds a new attachment from raw data * * @param {string} data The data of the attachment * @param {string} formatType The mime-type of the attachment */ addAttachmentFromData(data, formatType = null) { this.addProperty(AttachmentProperty.fromData(data, formatType)); } /** * Adds a new attachment from a link * * @param {string} uri The URI of the attachment * @param {string} formatType The mime-type of the attachment */ addAttachmentFromLink(uri, formatType = null) { this.addProperty(AttachmentProperty.fromLink(uri, formatType)); } /** * Adds a new contact * * @url https://tools.ietf.org/html/rfc5545#section-3.8.4.2 * * @param {string} contact The textual contact description to add */ addContact(contact) { this.addProperty(new TextProperty("CONTACT", contact)); } /** * Adds a new comment * * @url https://tools.ietf.org/html/rfc5545#section-3.8.1.4 * * @param {string} comment The comment to add */ addComment(comment) { this.addProperty(new TextProperty("COMMENT", comment)); } /** * Adds a new image from raw data * * @param {string} data Data of the image to add * @param {string=} display What display-type the image is optimized for * @param {string=} formatType The mime-type of the image */ addImageFromData(data, display = null, formatType = null) { this.addProperty(ImageProperty.fromData(data, display, formatType)); } /** * Adds a new image from a link * * @param {string} uri The URI of the image to add * @param {string=} display What display-type the image is optimized for * @param {string=} formatType The mime-type of the image */ addImageFromLink(uri, display = null, formatType = null) { this.addProperty(ImageProperty.fromLink(uri, display, formatType)); } /** * Creates a new RELATED-TO property based on a relation-type and id * and adds it to this object * * @param {string} relType The type of relation to add * @param {string} relId The id of the related calendar-document */ addRelation(relType, relId) { this.addProperty(RelationProperty.fromRelTypeAndId(relType, relId)); } /** * Creates a new REQUEST-STATUS property based on code and message * and adds it to this object * * @param {number} code The status-code of the request status * @param {string} message The message of the request status */ addRequestStatus(code, message) { this.addProperty(RequestStatusProperty.fromCodeAndMessage(code, message)); } /** * Adds a new absolute alarm based on action and trigger time * * @param {string} action The type of alarm Action * @param {DateTimeValue} alarmTime The trigger time of the alarm * @return {AlarmComponent} */ addAbsoluteAlarm(action, alarmTime) { const alarmComp = new AlarmComponent("VALARM", [ ["action", action], TriggerProperty.fromAbsolute(alarmTime) ]); this.addComponent(alarmComp); return alarmComp; } /** * Adds a new relative alarm based on action, trigger time and relativeTo parameter * * @param {string} action The type of alarm Action * @param {DurationValue} alarmOffset The trigger time of the alarm * @param {boolean=} relatedToStart Whether or not the alarm is related to the event's start * @return {AlarmComponent} */ addRelativeAlarm(action, alarmOffset, relatedToStart = true) { const alarmComp = new AlarmComponent("VALARM", [ ["action", action], TriggerProperty.fromRelativeAndRelated(alarmOffset, relatedToStart) ]); this.addComponent(alarmComp); return alarmComp; } /** * Marks a certain property as edited * * @param {string} propertyName The name of the property */ markPropertyAsDirty(propertyName) { this.markDirty(); const props = [ "DTSTART", "DTEND", "DURATION", "RRULE", "RDATE", "EXDATE", "STATUS", ...getConfig("property-list-significant-change", []) ]; if (props.includes(uc(propertyName))) { this.markChangesAsSignificant(); } } /** * Marks a certain component as edited * * @param {string} componentName The name of the component */ markSubComponentAsDirty(componentName) { this.markDirty(); if (getConfig("component-list-significant-change", []).includes(componentName)) { this.markChangesAsSignificant(); } } /** * Returns whether or not this component is dirty * * @return {boolean} */ isDirty() { return this._dirty || this._significantChange; } /** * Marks this object as dirty */ markDirty() { this._dirty = true; } /** * Marks changes as significant. Can be called by the program using this lib */ markChangesAsSignificant() { this._significantChange = true; } /** * Updates the event after modifications. * * @return {boolean} true if last-modified was updated */ undirtify() { if (!this.isDirty()) { return false; } if (!this.hasProperty("SEQUENCE")) { this.sequence = 0; } this.updatePropertyWithValue("DTSTAMP", DateTimeValue.fromJSDate(dateFactory(), true)); this.updatePropertyWithValue("LAST-MODIFIED", DateTimeValue.fromJSDate(dateFactory(), true)); if (this._significantChange) { this.sequence++; } this.resetDirty(); return true; } /** * Resets the dirty indicators without updating DTSTAMP or LAST-MODIFIED */ resetDirty() { this._dirty = false; this._significantChange = false; } /** * @inheritDoc */ updatePropertyWithValue(propertyName, value) { super.updatePropertyWithValue(propertyName, value); if (uc(propertyName) === "UID") { this._cachedId = null; } this.markPropertyAsDirty(propertyName); } /** * @inheritDoc */ addProperty(property) { this.markPropertyAsDirty(property.name); property.subscribe(() => this.markPropertyAsDirty(property.name)); return super.addProperty(property); } /** * @inheritDoc */ deleteProperty(property) { this.markPropertyAsDirty(property.name); return super.deleteProperty(property); } /** * @inheritDoc */ deleteAllProperties(propertyName) { this.markPropertyAsDirty(propertyName); return super.deleteAllProperties(propertyName); } /** * @inheritDoc */ addComponent(component) { this.markSubComponentAsDirty(component.name); component.subscribe(() => this.markSubComponentAsDirty(component.name)); return super.addComponent(component); } /** * @inheritDoc */ deleteComponent(component) { this.markSubComponentAsDirty(component.name); return super.deleteComponent(component); } /** * @inheritDoc */ deleteAllComponents(componentName) { this.markSubComponentAsDirty(componentName); return super.deleteAllComponents(componentName); } /** * Gets a recurrence-id that has to be used to refer to this event. * This is used for recurrence-management * * @return {DateTimeValue|null} */ getReferenceRecurrenceId() { if (this.originalRecurrenceId) { return this.originalRecurrenceId; } else if (this.recurrenceId) { return this.recurrenceId; } else if (this.startDate) { return this.startDate; } return null; } /** * Overrides the master item with this one * * @private */ _overridePrimaryItem() { const oldStartDate = this.primaryItem.startDate; for (const property of this.primaryItem.getPropertyIterator()) { this.primaryItem.deleteProperty(property); } for (const property of this.getPropertyIterator()) { this.primaryItem.addProperty(property); } this.recurrenceManager.resetCache(); if (this.startDate.compare(oldStartDate) !== 0) { this.recurrenceManager.updateStartDateOfMasterItem(this.startDate, oldStartDate); } } /** * @inheritDoc */ static _getConstructorForComponentName(componentName) { return getConstructorForComponentName$1(componentName); } /** * @inheritDoc */ static fromICALJs(...args) { const comp = super.fromICALJs(...args); comp.resetDirty(); return comp; } } advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, { name: "stampTime", iCalendarName: "DTSTAMP" }); advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, { name: "recurrenceId", iCalendarName: "RECURRENCE-ID" }); advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, "color"); advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, { name: "creationTime", iCalendarName: "CREATED" }); advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, { name: "modificationTime", iCalendarName: "LAST-MODIFIED" }); advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, "organizer"); advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, "sequence"); advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, "status"); advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, "url"); advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, { name: "title", iCalendarName: "SUMMARY" }); advertiseSingleOccurrenceProperty(AbstractRecurringComponent.prototype, { name: "accessClass", iCalendarName: "class", allowedValues: ["PUBLIC", "PRIVATE", "CONFIDENTIAL"], defaultValue: "PUBLIC", unknownValue: "PRIVATE" }); advertiseMultiValueStringPropertySeparatedByLang(AbstractRecurringComponent.prototype, { name: "category", pluralName: "categories", iCalendarName: "CATEGORIES" }); advertiseMultipleOccurrenceProperty(AbstractRecurringComponent.prototype, { name: "attendee" }); advertiseMultipleOccurrenceProperty(AbstractRecurringComponent.prototype, { name: "attachment", iCalendarName: "ATTACH" }); advertiseMultipleOccurrenceProperty(AbstractRecurringComponent.prototype, { name: "relation", iCalendarName: "RELATED-TO" }); advertiseMultipleOccurrenceProperty(AbstractRecurringComponent.prototype, "comment"); advertiseMultipleOccurrenceProperty(AbstractRecurringComponent.prototype, "contact"); advertiseMultipleOccurrenceProperty(AbstractRecurringComponent.prototype, "image"); advertiseMultipleOccurrenceProperty(AbstractRecurringComponent.prototype, { name: "requestStatus", pluralName: "requestStatus", iCalendarName: "REQUEST-STATUS" }); advertiseComponent(AbstractRecurringComponent.prototype, "alarm"); /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ function getTypeOfBirthdayEvent(eventComponent) { return eventComponent.getFirstPropertyFirstValue("X-NEXTCLOUD-BC-FIELD-TYPE"); } function getIconForBirthday(eventComponent) { const birthdayType = getTypeOfBirthdayEvent(eventComponent); switch (birthdayType) { case "BDAY": return "🎂"; case "DEATHDATE": return "⚰️"; case "ANNIVERSARY": return "💍"; default: return null; } } function getAgeOfBirthday(eventComponent, yearOfOccurrence) { if (!eventComponent.hasProperty("X-NEXTCLOUD-BC-YEAR")) { return null; } const yearOfBirth = eventComponent.getFirstPropertyFirstValue("X-NEXTCLOUD-BC-YEAR"); return parseInt(yearOfOccurrence, 10) - parseInt(yearOfBirth, 10); } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class EventComponent extends AbstractRecurringComponent { /** * Returns whether this event is an all-day event * * @return {boolean} */ isAllDay() { return this.startDate.isDate && this.endDate.isDate; } /** * Checks whether it's possible to switch from date-time to date or vise-versa * * @return {boolean} */ canModifyAllDay() { return !this.recurrenceManager.masterItem.isRecurring(); } /** * Gets the calculated end-date of the event * * Quote from RFC 5545 3.6.1: * The "DTSTART" property for a "VEVENT" specifies the inclusive * start of the event. For recurring events, it also specifies the * very first instance in the recurrence set. The "DTEND" property * for a "VEVENT" calendar component specifies the non-inclusive end * of the event. For cases where a "VEVENT" calendar component * specifies a "DTSTART" property with a DATE value type but no * "DTEND" nor "DURATION" property, the event's duration is taken to * be one day. For cases where a "VEVENT" calendar component * specifies a "DTSTART" property with a DATE-TIME value type but no * "DTEND" property, the event ends on the same calendar date and * time of day specified by the "DTSTART" property. * * @return {DateTimeValue} */ get endDate() { if (this.hasProperty("dtend")) { return this.getFirstPropertyFirstValue("dtend"); } const dtend = this.startDate.clone(); if (this.hasProperty("duration")) { dtend.addDuration(this.getFirstPropertyFirstValue("duration")); } else if (this.startDate.isDate) { dtend.addDuration(DurationValue.fromSeconds(60 * 60 * 24)); } return dtend; } /** * Sets the end time of the event * * @param {DateTimeValue} end The end of the event */ set endDate(end) { this.deleteAllProperties("duration"); this.updatePropertyWithValue("dtend", end); } /** * Gets the calculated duration of the event * * @return {DurationValue} */ get duration() { if (this.hasProperty("duration")) { return this.getFirstPropertyFirstValue("duration"); } return this.startDate.subtractDateWithTimezone(this.endDate); } /** * Sets the calculated duration of the event * * @param {DurationValue} duration The duration of the event */ set duration(duration) { this.deleteAllProperties("dtend"); this.updatePropertyWithValue("duration", duration); } /** * Sets the geographical position based on latitude and longitude * * @url https://tools.ietf.org/html/rfc5545#section-3.8.1.6 * * @param {number} lat - latitude * @param {number} long - longitude */ setGeographicalPositionFromLatitudeAndLongitude(lat, long) { this.deleteAllProperties("GEO"); this.addProperty(GeoProperty.fromPosition(lat, long)); } /** * Adds a new conference property based on URI, label and features * * @url https://tools.ietf.org/html/rfc7986#section-5.11 * * @param {string} uri The URI of the conference system * @param {string=} label The label for the conference system * @param {string[]=} features The features of the conference system */ addConference(uri, label = null, features = null) { this._modify(); this.addProperty(ConferenceProperty.fromURILabelAndFeatures(uri, label, features)); } /** * Adds a duration to the start of the event * * @param {DurationValue} duration The duration to add */ addDurationToStart(duration) { this.startDate.addDuration(duration); } /** * Adds a duration to the end of the event * * @param {DurationValue} duration The duration to add */ addDurationToEnd(duration) { const endDate = this.endDate; endDate.addDuration(duration); this.endDate = endDate; } /** * Shifts the entire event by the given duration * * @param {DurationValue} delta The duration to shift event by * @param {boolean} allDay Whether the updated event should be all-day or not * @param {Timezone} defaultTimezone The default timezone if moving from all-day to timed event * @param {DurationValue} defaultAllDayDuration The default all-day duration if moving from timed to all-day * @param {DurationValue} defaultTimedDuration The default timed duration if moving from all-day to timed */ shiftByDuration(delta, allDay, defaultTimezone, defaultAllDayDuration, defaultTimedDuration) { const currentAllDay = this.isAllDay(); if (currentAllDay !== allDay && !this.canModifyAllDay()) { throw new TypeError("Can't modify all-day of this event"); } this.startDate.isDate = allDay; this.startDate.addDuration(delta); if (currentAllDay && !allDay) { this.startDate.replaceTimezone(defaultTimezone); this.endDate = this.startDate.clone(); this.endDate.addDuration(defaultTimedDuration); } if (!currentAllDay && allDay) { this.endDate = this.startDate.clone(); this.endDate.addDuration(defaultAllDayDuration); } if (currentAllDay === allDay) { const endDate = this.endDate; endDate.addDuration(delta); this.endDate = endDate; } } /** * Checks if this is a birthday event * * @return {boolean} */ isBirthdayEvent() { return getTypeOfBirthdayEvent(this) === "BDAY"; } /** * Gets the icon to the birthday event * * @return {string} */ getIconForBirthdayEvent() { return getIconForBirthday(this); } /** * Calculates the age of the birthday * * @return {number} */ getAgeForBirthdayEvent() { return getAgeOfBirthday(this, this.startDate.year); } /** * Serializes the entire series to ICS * * @return {string} */ toICSEntireSeries() { return this.root.toICS(); } /** * Serializes exactly this recurrence to ICS * It removes all recurrence information * * @return {string} */ toICSThisOccurrence() { const clone = this.clone(); clone.deleteAllProperties("RRULE"); clone.deleteAllProperties("EXRULE"); clone.deleteAllProperties("RDATE"); clone.deleteAllProperties("EXDATE"); clone.deleteAllProperties("RECURRENCE-ID"); clone.root = clone.root.constructor.fromEmpty(); clone.parent = clone.root; clone.root.addComponent(clone); return clone.root.toICS(); } /** * Checks if this event is in a given time-frame * * @param {DateTimeValue} start Start of time-range to check * @param {DateTimeValue} end End of time-range to check * @return {boolean} */ isInTimeFrame(start, end) { return start.compare(this.endDate) <= 0 && end.compare(this.startDate) >= 0; } } advertiseSingleOccurrenceProperty(EventComponent.prototype, { name: "timeTransparency", iCalendarName: "TRANSP", allowedValues: ["OPAQUE", "TRANSPARENT"], defaultValue: "OPAQUE" }); advertiseSingleOccurrenceProperty(EventComponent.prototype, "description"); advertiseSingleOccurrenceProperty(EventComponent.prototype, { name: "geographicalPosition", iCalendarName: "GEO" }); advertiseSingleOccurrenceProperty(EventComponent.prototype, "location"); advertiseSingleOccurrenceProperty(EventComponent.prototype, { name: "priority", allowedValues: Array(9).keys(), defaultValue: 0, unknownValue: 0 }); advertiseMultiValueStringPropertySeparatedByLang(EventComponent.prototype, { name: "resource", iCalendarName: "RESOURCES" }); advertiseMultipleOccurrenceProperty(EventComponent.prototype, "conference"); /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class FreeBusyComponent extends AbstractComponent { /** * Gets the start-date of the FreeBusy component * * @return {DateTimeValue} */ get startDate() { return this.getFirstPropertyFirstValue("DTSTART"); } /** * Sets the start-date of the FreeBusy component * * @param {DateTimeValue} startDate The start of the queried time-range */ set startDate(startDate) { this._modify(); this.updatePropertyWithValue("DTSTART", startDate.getInTimezone(timezones.Timezone.utc)); } /** * Gets the end-date of the FreeBusy component * * @return {DateTimeValue} */ get endDate() { return this.getFirstPropertyFirstValue("DTEND"); } /** * Sets the start-date of the FreeBusy component * * @param {DateTimeValue} endDate The end of the queried time-range */ set endDate(endDate) { this._modify(); this.updatePropertyWithValue("DTEND", endDate.getInTimezone(timezones.Timezone.utc)); } /** * Gets an iterator over all FreeBusyProperties */ *getFreeBusyIterator() { yield* this.getPropertyIterator("FREEBUSY"); } /** * Adds a new attendee based on their name and email-address * * @url https://tools.ietf.org/html/rfc5545#section-3.8.4.1 * * @param {string} name The name of the attendee to add * @param {string} email The email-address of the attendee to add */ addAttendeeFromNameAndEMail(name, email) { this._modify(); this.addProperty(AttendeeProperty.fromNameAndEMail(name, email)); } /** * Sets the organiser property from common-name and email address * * @url https://tools.ietf.org/html/rfc5545#section-3.8.4.3 * * @param {string} name The name of the organizer * @param {string} email The email-address of the organizer */ setOrganizerFromNameAndEMail(name, email) { this._modify(); this.deleteAllProperties("ORGANIZER"); this.addProperty(AttendeeProperty.fromNameAndEMail(name, email, true)); } } advertiseSingleOccurrenceProperty(FreeBusyComponent.prototype, "organizer"); advertiseSingleOccurrenceProperty(FreeBusyComponent.prototype, "uid"); advertiseMultipleOccurrenceProperty(FreeBusyComponent.prototype, "attendee"); /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class JournalComponent extends AbstractRecurringComponent { /** * Adds a new description property * * @url https://tools.ietf.org/html/rfc5545#section-3.8.1.5 * * @param {string} description The description text */ addDescription(description) { this.addProperty(new TextProperty("DESCRIPTION", description)); } } advertiseMultipleOccurrenceProperty(JournalComponent.prototype, "description"); /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class TimezoneComponent extends AbstractComponent { /** * Returns a calendar-js Timezone object * * @return {Timezone} */ toTimezone() { return new timezones.Timezone(this.toICALJs()); } } advertiseSingleOccurrenceProperty(TimezoneComponent.prototype, { name: "timezoneId", iCalendarName: "tzid" }); /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ToDoComponent extends AbstractRecurringComponent { /** * Returns whether this event is an all-day event * * @return {boolean} */ isAllDay() { const propertiesToCheck = ["DTSTART", "DUE"]; for (const propertyToCheck of propertiesToCheck) { if (this.hasProperty(propertyToCheck)) { return this.getFirstPropertyFirstValue(propertyToCheck).isDate; } } return true; } /** * Checks whether it's possible to switch from date-time to date or vise-versa * * @return {boolean} */ canModifyAllDay() { if (!this.hasProperty("dtstart") && !this.hasProperty("due")) { return false; } return !this.recurrenceManager.masterItem.isRecurring(); } /** * Gets the calculated end-date of the task * * If there is a due-date, we will just return that. * If there is a start-date and a duration, we will * calculate the end-date based on that. * * If there is neither a due-date nor a combination * of start-date and duration, we just return null * * @return {DateTimeValue|null} */ get endDate() { if (this.hasProperty("due")) { return this.getFirstPropertyFirstValue("due"); } if (!this.hasProperty("dtstart") || !this.hasProperty("duration")) { return null; } const endDate = this.startDate.clone(); endDate.addDuration(this.getFirstPropertyFirstValue("duration")); return endDate; } /** * Shifts the entire task by the given duration * * @param {DurationValue} delta The duration to shift event by * @param {boolean} allDay Whether the updated event should be all-day or not * @param {Timezone} defaultTimezone The default timezone if moving from all-day to timed event * @param {DurationValue} defaultAllDayDuration The default all-day duration if moving from timed to all-day * @param {DurationValue} defaultTimedDuration The default timed duration if moving from all-day to timed */ shiftByDuration(delta, allDay, defaultTimezone, defaultAllDayDuration, defaultTimedDuration) { const currentAllDay = this.isAllDay(); if (!this.hasProperty("dtstart") && !this.hasProperty("due")) { throw new TypeError("This task does not have a start-date nor due-date"); } if (currentAllDay !== allDay && !this.canModifyAllDay()) { throw new TypeError("Can't modify all-day of this todo"); } if (this.hasProperty("dtstart")) { this.startDate.isDate = allDay; this.startDate.addDuration(delta); if (currentAllDay && !allDay) { this.startDate.replaceTimezone(defaultTimezone); } } if (this.hasProperty("due")) { this.dueTime.isDate = allDay; this.dueTime.addDuration(delta); if (currentAllDay && !allDay) { this.dueTime.replaceTimezone(defaultTimezone); } } } /** * Checks if this event is in a given time-frame * * @param {DateTimeValue} start Start of time-range to check * @param {DateTimeValue} end End of time-range to check * @return {boolean} */ isInTimeFrame(start, end) { if (!this.hasProperty("dtstart") && !this.hasProperty("due")) { return true; } if (!this.hasProperty("dtstart") && this.hasProperty("due")) { return start.compare(this.endDate) <= 0; } return start.compare(this.endDate) <= 0 && end.compare(this.startDate) >= 0; } /** * Gets the geographical position property * * @return {GeoProperty} */ get geographicalPosition() { return this.getFirstProperty("GEO"); } /** * Sets the geographical position based on latitude and longitude * * @url https://tools.ietf.org/html/rfc5545#section-3.8.1.6 * * @param {number} lat - latitude * @param {number} long - longitude */ setGeographicalPositionFromLatitudeAndLongitude(lat, long) { this.deleteAllProperties("GEO"); this.addProperty(GeoProperty.fromPosition(lat, long)); } /** * Adds a new conference property based on URI, label and features * * @url https://tools.ietf.org/html/rfc7986#section-5.11 * * @param {string} uri The URI of the conference * @param {string=} label The label of the conference * @param {string[]=} features Supported features of conference-system */ addConference(uri, label = null, features = null) { this.addProperty(ConferenceProperty.fromURILabelAndFeatures(uri, label, features)); } /** * Gets a recurrence-id that has to be used to refer to this task. * This is used for recurrence-management. * * Gracefully handles the case where a task has no start-date, but a due-date. * * @return {DateTimeValue|null} */ getReferenceRecurrenceId() { return super.getReferenceRecurrenceId() ?? this.endDate; } } advertiseSingleOccurrenceProperty(ToDoComponent.prototype, { name: "completedTime", iCalendarName: "COMPLETED" }); advertiseSingleOccurrenceProperty(ToDoComponent.prototype, { name: "dueTime", iCalendarName: "DUE" }); advertiseSingleOccurrenceProperty(ToDoComponent.prototype, { name: "duration" }); advertiseSingleOccurrenceProperty(ToDoComponent.prototype, { name: "percent", iCalendarName: "PERCENT-COMPLETE" }); advertiseSingleOccurrenceProperty(ToDoComponent.prototype, "description"); advertiseSingleOccurrenceProperty(ToDoComponent.prototype, "location"); advertiseSingleOccurrenceProperty(ToDoComponent.prototype, { name: "priority", allowedValues: Array.from(Array(10).keys()), defaultValue: 0, unknownValue: 0 }); advertiseMultiValueStringPropertySeparatedByLang(ToDoComponent.prototype, { name: "resource", iCalendarName: "RESOURCES" }); advertiseMultipleOccurrenceProperty(ToDoComponent.prototype, "conference"); /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ function getConstructorForComponentName(compName) { switch (uc(compName)) { case "VEVENT": return EventComponent; case "VFREEBUSY": return FreeBusyComponent; case "VJOURNAL": return JournalComponent; case "VTIMEZONE": return TimezoneComponent; case "VTODO": return ToDoComponent; default: return AbstractComponent; } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class CalendarComponent extends AbstractComponent { /** * Constructor * * @inheritDoc */ constructor(name = "VCALENDAR", properties = [], components = []) { super(name, properties, components); this.root = this; this.parent = null; } /** * Gets an iterator over all VTIMEZONE components */ *getTimezoneIterator() { yield* this.getComponentIterator("vtimezone"); } /** * Gets an iterator over all VObject components */ *getVObjectIterator() { yield* this.getEventIterator(); yield* this.getJournalIterator(); yield* this.getTodoIterator(); } /** * Gets an iterator over all VEVENT components */ *getEventIterator() { yield* this.getComponentIterator("vevent"); } /** * Gets an iterator over all VFREEBUSY components */ *getFreebusyIterator() { yield* this.getComponentIterator("vfreebusy"); } /** * Gets an iterator over all VJOURNAL components */ *getJournalIterator() { yield* this.getComponentIterator("vjournal"); } /** * Gets an iterator over all VTODO components */ *getTodoIterator() { yield* this.getComponentIterator("vtodo"); } /** * @inheritDoc */ static _getConstructorForComponentName(componentName) { return getConstructorForComponentName(componentName); } /** * Converts this calendar component into text/calendar * * @param {boolean} cleanUpTimezones Whether or not to clean up timezone data * @return {string} */ toICS(cleanUpTimezones = true) { for (const vObject of this.getVObjectIterator()) { vObject.undirtify(); } const icalRoot = this.toICALJs(); if (cleanUpTimezones) { ICAL__default.default.helpers.updateTimezones(icalRoot); } return icalRoot.toString(); } /** * Creates a new empty calendar-component * * @param {[string][]=} additionalProps Additional props to add to empty calendar-document * @return {CalendarComponent} */ static fromEmpty(additionalProps = []) { return new this("VCALENDAR", [ ["prodid", getConfig("PRODID", "-//IDN georgehrke.com//calendar-js//EN")], ["calscale", "GREGORIAN"], ["version", "2.0"] ].concat(additionalProps)); } /** * Creates a new calendar-component with a method * * @param {string} method The method for the calendar-document * @return {CalendarComponent} */ static fromMethod(method) { return this.fromEmpty([["method", method]]); } /** * @inheritDoc */ static fromICALJs(icalValue) { const comp = super.fromICALJs(icalValue); comp.root = comp; return comp; } } advertiseSingleOccurrenceProperty(CalendarComponent.prototype, { name: "productId", iCalendarName: "PRODID" }); advertiseSingleOccurrenceProperty(CalendarComponent.prototype, { name: "version" }); advertiseSingleOccurrenceProperty(CalendarComponent.prototype, { name: "calendarScale", iCalendarName: "CALSCALE", defaultValue: "GREGORIAN" }); advertiseSingleOccurrenceProperty(CalendarComponent.prototype, { name: "method" }); /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class AbstractRepairStep { /** * @class */ constructor() { if (new.target === AbstractRepairStep) { throw new TypeError("Cannot instantiate abstract class AbstractRepairStep"); } } /** * @param {string} input String representation of the data to repair */ repair(input) { throw new TypeError("Abstract method not implemented by subclass"); } /** * @return {number} */ static priority() { return 0; } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ICalendarAddMissingUIDRepairStep extends AbstractRepairStep { /** * Please see the corresponding test file for an example of broken calendar-data * * @inheritDoc */ repair(ics) { return ics.replace(/^BEGIN:(VEVENT|VTODO|VJOURNAL)$(((?!^END:(VEVENT|VTODO|VJOURNAL)$)(?!^UID.*$)(.|\n))*)^END:(VEVENT|VTODO|VJOURNAL)$\n/gm, (match, vobjectName, vObjectBlock) => { return "BEGIN:" + vobjectName + "\r\nUID:" + uuid.v4() + vObjectBlock + "END:" + vobjectName + "\r\n"; }); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ICalendarAddMissingValueDateDoubleColonRepairStep extends AbstractRepairStep { /** * Please see the corresponding test file for an example of broken calendar-data * * @inheritDoc */ repair(ics) { return ics.replace(/^(DTSTART|DTEND)(.*):([0-9]{8})T(::)$/gm, (match, propName, parameters, date) => { return propName + ";VALUE=DATE:" + date; }); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ICalendarAddMissingValueDateRepairStep extends AbstractRepairStep { /** * Please see the corresponding test file for an example of broken calendar-data * * @inheritDoc */ repair(ics) { return ics.replace(/^(DTSTART|DTEND|EXDATE)(((?!VALUE=DATE).)*):([0-9]{8})$/gm, (match, propName, parameters, _, date) => { return propName + parameters + ";VALUE=DATE:" + date; }); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ICalendarEmptyTriggerRepairStep extends AbstractRepairStep { /** * Please see the corresponding test file for an example of broken calendar-data * * @inheritDoc */ repair(ics) { return ics.replace(/^TRIGGER:P$/gm, "TRIGGER:P0D").replace(/^TRIGGER:-P$/gm, "TRIGGER:P0D"); } } /** * @copyright Copyright (c) 2020 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ICalendarIllegalCreatedRepairStep extends AbstractRepairStep { /** * Please see the corresponding test file for an example of broken calendar-data * * @inheritDoc */ repair(ics) { return ics.replace(/^CREATED:00001231T000000Z$/gm, "CREATED:19700101T000000Z"); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ICalendarMultipleVCalendarBlocksRepairStep extends AbstractRepairStep { /** * Please see the corresponding test file for an example of broken calendar-data * * @inheritDoc */ repair(ics) { let containsProdId = false; let containsVersion = false; let containsCalscale = false; const includedTimezones = /* @__PURE__ */ new Set(); return ics.replace(/^END:VCALENDAR$(((?!^BEGIN:)(.|\n))*)^BEGIN:VCALENDAR$\n/gm, "").replace(/^PRODID:(.*)$\n/gm, (match) => { if (containsProdId) { return ""; } containsProdId = true; return match; }).replace(/^VERSION:(.*)$\n/gm, (match) => { if (containsVersion) { return ""; } containsVersion = true; return match; }).replace(/^CALSCALE:(.*)$\n/gm, (match) => { if (containsCalscale) { return ""; } containsCalscale = true; return match; }).replace(/^BEGIN:VTIMEZONE$(((?!^END:VTIMEZONE$)(.|\n))*)^END:VTIMEZONE$\n/gm, (match) => { const tzidMatcher = match.match(/^TZID:(.*)$/gm); if (tzidMatcher === null) { return ""; } const tzid = uc(tzidMatcher[0].slice(5)); if (includedTimezones.has(tzid)) { return ""; } includedTimezones.add(tzid); return match; }); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ICalendarRemoveXNCGroupIdRepairStep extends AbstractRepairStep { /** * Please see the corresponding test file for an example of broken calendar-data * * @inheritDoc */ repair(ics) { return ics.replace(/(^.*)(;X-NC-GROUP-ID=\d+)(:.*$)/gm, "$1$3"); } } /** * @copyright Copyright (c) 2024 Sanskar Soni * * @author Sanskar Soni * * @license AGPL-3.0-or-later * * 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 . * */ class ICalendarRemoveUnicodeSpecialNoncharactersRepairStep extends AbstractRepairStep { /** * Please see the corresponding test file for an example of broken calendar-data * * @inheritDoc */ repair(ics) { return ics.replace(/(\uFFFF|\uFFFE)/g, ""); } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ function* getRepairSteps() { yield ICalendarAddMissingUIDRepairStep; yield ICalendarAddMissingValueDateDoubleColonRepairStep; yield ICalendarAddMissingValueDateRepairStep; yield ICalendarEmptyTriggerRepairStep; yield ICalendarIllegalCreatedRepairStep; yield ICalendarMultipleVCalendarBlocksRepairStep; yield ICalendarRemoveXNCGroupIdRepairStep; yield ICalendarRemoveUnicodeSpecialNoncharactersRepairStep; } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ICalendarParser extends AbstractParser { /** * @inheritDoc */ constructor(...args) { super(...args); this._rawData = null; this._calendarComponent = null; this._containsVEvents = false; this._containsVJournals = false; this._containsVTodos = false; this._containsVFreeBusy = false; this._items = /* @__PURE__ */ new Map(); this._masterItems = /* @__PURE__ */ new Map(); this._recurrenceExceptionItems = /* @__PURE__ */ new Map(); this._forgedMasterItems = /* @__PURE__ */ new Map(); this._timezones = /* @__PURE__ */ new Map(); this._requiredTimezones = /* @__PURE__ */ new Map(); this._defaultTimezoneManager = timezones.getTimezoneManager(); } /** * Parses the actual calendar-data * * @param {string} ics The icalendar data to parse */ parse(ics) { this._rawData = ics; this._applyRepairSteps(); 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); } 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__default.default.parse(this._rawData); const icalComp = new ICAL__default.default.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].slice(5); const timezone = new timezones.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); } } } 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) { 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); 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 = /* @__PURE__ */ 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); if (timezone.timezoneId !== requiredTimezone) { this._replaceTimezoneWithAnotherOne(calendarComp, requiredTimezone, timezone.timezoneId); } const timezoneComponent = TimezoneComponent.fromICALJs(timezone.toICALJs()); 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); } } } 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"]; } } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class ParserManager { /** * Constructor */ constructor() { this._parsers = []; } /** * Get a list of all supported file-types * * @return {string[]} */ getAllSupportedFileTypes() { return this._parsers.reduce( (allFileTypes, parser) => allFileTypes.concat(parser.getMimeTypes()), [] ); } /** * Get an instance of a parser for one specific file-type * * @param {string} fileType The mime-type to get a parser for * @param {object=} options Options destructuring object * @param {boolean=} options.extractGlobalProperties Whether or not to preserve properties from the VCALENDAR component (defaults to false) * @param {boolean=} options.removeRSVPForAttendees Whether or not to remove RSVP from attendees (defaults to false) * @param {boolean=} options.includeTimezones Whether or not to include timezones (defaults to false) * @param {boolean=} options.preserveMethod Whether or not to preserve the iCalendar method (defaults to false) * @param {boolean=} options.processFreeBusy Whether or not to process VFreeBusy components (defaults to false) * * @return {AbstractParser} */ getParserForFileType(fileType, options) { const Parser = this._parsers.find( (parser) => parser.getMimeTypes().includes(fileType) ); if (!Parser) { throw new TypeError("Unknown file-type."); } return new Parser(options); } /** * Registers a parser * * @param {Function} parser The parser to register */ registerParser(parser) { this._parsers.push(parser); } } function getParserManager() { const parserManager = new ParserManager(); parserManager.registerParser(ICalendarParser); return parserManager; } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @license AGPL-3.0-or-later * * 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 . * */ class IllegalValueError extends Error { } /** * @copyright Copyright (c) 2019 Georg Ehrke * * @author Georg Ehrke * * @author Richard Steinmetz * * @license AGPL-3.0-or-later * * 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 . * */ function* parseICSAndGetAllOccurrencesBetween(ics, start, end) { const parserManager = getParserManager(); const icsParser = parserManager.getParserForFileType("text/calendar"); icsParser.parse(ics); const objectIterator = icsParser.getItemIterator(); const calendarComp = objectIterator.next().value; if (calendarComp === void 0) { return; } const vObjectIterator = calendarComp.getVObjectIterator(); const firstVObject = vObjectIterator.next().value; if (firstVObject === void 0) { return; } yield* firstVObject.recurrenceManager.getAllOccurrencesBetweenIterator(start, end); } function createEvent(start, end) { const calendar = CalendarComponent.fromEmpty(); const eventComponent = new EventComponent("VEVENT"); eventComponent.updatePropertyWithValue("CREATED", DateTimeValue.fromJSDate(dateFactory(), true)); eventComponent.updatePropertyWithValue("DTSTAMP", DateTimeValue.fromJSDate(dateFactory(), true)); eventComponent.updatePropertyWithValue("LAST-MODIFIED", DateTimeValue.fromJSDate(dateFactory(), true)); eventComponent.updatePropertyWithValue("SEQUENCE", 0); eventComponent.updatePropertyWithValue("UID", uuid.v4()); eventComponent.updatePropertyWithValue("DTSTART", start); eventComponent.updatePropertyWithValue("DTEND", end); calendar.addComponent(eventComponent); eventComponent.recurrenceManager = new RecurrenceManager(eventComponent); return calendar; } function createFreeBusyRequest(start, end, organizer, attendees) { const calendar = CalendarComponent.fromMethod("REQUEST"); const freeBusyComponent = new FreeBusyComponent("VFREEBUSY"); freeBusyComponent.updatePropertyWithValue("DTSTAMP", DateTimeValue.fromJSDate(dateFactory(), true)); freeBusyComponent.updatePropertyWithValue("UID", uuid.v4()); freeBusyComponent.updatePropertyWithValue("DTSTART", start.clone().getInUTC()); freeBusyComponent.updatePropertyWithValue("DTEND", end.clone().getInUTC()); freeBusyComponent.addProperty(organizer.clone()); for (const attendee of attendees) { const clonedAttendee = attendee.clone(); clonedAttendee.deleteParameter("ROLE"); clonedAttendee.deleteParameter("CUTYPE"); clonedAttendee.deleteParameter("RSVP"); clonedAttendee.deleteParameter("PARTSTAT"); clonedAttendee.deleteParameter("REQUEST-STATUS"); clonedAttendee.deleteParameter("LANGUAGE"); freeBusyComponent.addProperty(clonedAttendee); } calendar.addComponent(freeBusyComponent); return calendar; } exports.AbstractComponent = AbstractComponent; exports.AbstractParser = AbstractParser; exports.AbstractRecurringComponent = AbstractRecurringComponent; exports.AbstractValue = AbstractValue; exports.AlarmComponent = AlarmComponent; exports.AttachmentProperty = AttachmentProperty; exports.AttendeeProperty = AttendeeProperty; exports.BinaryValue = BinaryValue; exports.CalendarComponent = CalendarComponent; exports.ConferenceProperty = ConferenceProperty; exports.DateTimeValue = DateTimeValue; exports.DurationValue = DurationValue; exports.EventComponent = EventComponent; exports.ExpectedICalJSError = ExpectedICalJSError; exports.FreeBusyComponent = FreeBusyComponent; exports.FreeBusyProperty = FreeBusyProperty; exports.GeoProperty = GeoProperty; exports.ICalendarParser = ICalendarParser; exports.IllegalValueError = IllegalValueError; exports.ImageProperty = ImageProperty; exports.JournalComponent = JournalComponent; exports.ModificationNotAllowedError = ModificationNotAllowedError; exports.Parameter = Parameter; exports.ParserManager = ParserManager; exports.PeriodValue = PeriodValue; exports.Property = Property; exports.RecurValue = RecurValue; exports.RecurrenceManager = RecurrenceManager; exports.RecurringWithoutDtStartError = RecurringWithoutDtStartError; exports.RelationProperty = RelationProperty; exports.RequestStatusProperty = RequestStatusProperty; exports.TextProperty = TextProperty; exports.TimezoneComponent = TimezoneComponent; exports.ToDoComponent = ToDoComponent; exports.TriggerProperty = TriggerProperty; exports.UTCOffsetValue = UTCOffsetValue; exports.UnknownICALTypeError = UnknownICALTypeError; exports.createEvent = createEvent; exports.createFreeBusyRequest = createFreeBusyRequest; exports.getConstructorForICALType = getConstructorForICALType; exports.getConstructorForPropertyName = getConstructorForPropertyName; exports.getParserManager = getParserManager; exports.parseICSAndGetAllOccurrencesBetween = parseICSAndGetAllOccurrencesBetween; exports.setConfig = setConfig;