Skip to content

Commit

Permalink
fix: fixed calendar issue with generating random calendar UUIDs, adde…
Browse files Browse the repository at this point in the history
…d deleteCalendar method, prevent getCalendar from creating new calendar
  • Loading branch information
titanism committed Sep 2, 2024
1 parent 8a6c586 commit f46d6bc
Show file tree
Hide file tree
Showing 28 changed files with 171 additions and 77 deletions.
170 changes: 119 additions & 51 deletions caldav-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@
* SPDX-License-Identifier: BUSL-1.1
*/

const { randomUUID } = require('node:crypto');

const API = require('@ladjs/api');
const Boom = require('@hapi/boom');
const ICAL = require('ical.js');
const caldavAdapter = require('caldav-adapter');
const etag = require('etag');
const mongoose = require('mongoose');
const ms = require('ms');
const uuid = require('uuid');
const { boolean } = require('boolean');
const { isEmail } = require('validator');
Expand Down Expand Up @@ -464,6 +461,7 @@ class CalDAV extends API {
this.createEvent = this.createEvent.bind(this);
this.updateEvent = this.updateEvent.bind(this);
this.deleteEvent = this.deleteEvent.bind(this);
this.deleteCalendar = this.deleteCalendar.bind(this);
this.buildICS = this.buildICS.bind(this);
this.getCalendarId = this.getCalendarId.bind(this);
this.getETag = this.getETag.bind(this);
Expand Down Expand Up @@ -492,6 +490,7 @@ class CalDAV extends API {
createEvent: this.createEvent,
updateEvent: this.updateEvent,
deleteEvent: this.deleteEvent,
deleteCalendar: this.deleteCalendar,
buildICS: this.buildICS,
getCalendarId: this.getCalendarId,
getETag: this.getETag
Expand Down Expand Up @@ -859,19 +858,48 @@ class CalDAV extends API {
user
});

logger.debug('defaultCalendar', { defaultCalendar });
if (!defaultCalendar) {
await Calendars.create({
// db virtual helper
instance: this,
session: ctx.state.session,

// TODO: figure out why UUID calendars getting created constantly
// TODO: delete calendar endpoint not working (?)
// calendarId
calendarId: user.username,

// calendar obj
// NOTE: Android uses "Events" and most others use "Calendar" as default calendar name
name: ctx.translate('CALENDAR'),
description: config.urls.web,
prodId: `//forwardemail.net//caldav//${ctx.locale.toUpperCase()}`,
//
// NOTE: instead of using timezone from IP we use
// their last time zone set in a browser session
// (this is way more accurate and faster)
//
// here were some alternatives though during R&D:
// * <https://github.com/runk/node-maxmind>
// * <https://github.com/evansiroky/node-geo-tz>
// * <https://github.com/safing/mmdbmeld>
// * <https://github.com/sapics/ip-location-db>
//
timezone: ctx.state.session.user.timezone,
url: config.urls.web,
readonly: false,
synctoken: `${config.urls.web}/ns/sync-token/1`
});
}

// delete any UUID-generated calendar names
// (once a day)
const cache = await this.client.get(`calendar_check:${user.username}`);
if (!cache) {
const calendars = await Calendars.find(this, ctx.state.session, {});
for (const calendar of calendars) {
// if it was UUID and no events then delete it
if (uuid.validate(calendar.name)) {
// if calendar name is UUID or "Calendar" or ctx.translate("CALENDAR")
if (
uuid.validate(calendar.name) ||
calendar.name === 'Calendar' ||
calendar.name === ctx.translate('CALENDAR')
) {
// eslint-disable-next-line no-await-in-loop
const count = await CalendarEvents.countDocuments(
this,
Expand All @@ -888,12 +916,7 @@ class CalDAV extends API {
}
}

await this.client.set(
`calendar_check:${user.username}`,
true,
'PX',
ms('1d')
);
await this.client.set(`calendar_check:${user.username}`, true);
}

return user;
Expand All @@ -910,15 +933,25 @@ class CalDAV extends API {
timezone,
params: ctx.state.params
});
name = name || ctx.state.params.calendarId || randomUUID();
const calendarId = ctx.state.params.calendarId || name;

// if calendar already exists with calendarId value then return 405
// <https://github.com/sabre-io/dav/blob/da8c1f226f1c053849540a189262274ef6809d1c/tests/Sabre/CalDAV/PluginTest.php#L289>
const calendar = await this.getCalendar(ctx, {
calendarId: ctx.state.params.calendarId
});

if (calendar)
throw Boom.methodNotAllowed(
ctx.translateError('CALENDAR_ALREADY_EXISTS')
);

return Calendars.create({
// db virtual helper
instance: this,
session: ctx.state.session,

// calendarId
calendarId,
calendarId: ctx.state.params.calendarId || name,

// calendar obj
name,
Expand Down Expand Up @@ -948,38 +981,6 @@ class CalDAV extends API {
calendar = await Calendars.findOne(this, ctx.state.session, {
name: calendarId
});
if (!calendar)
calendar = await Calendars.create({
// db virtual helper
instance: this,
session: ctx.state.session,

// calendarId
calendarId,

// calendar obj
// NOTE: Android uses "Events" and most others use "Calendar" as default calendar name
name: mongoose.isObjectIdOrHexString(calendarId)
? ctx.translate('CALENDAR')
: calendarId,
description: config.urls.web,
prodId: `//forwardemail.net//caldav//${ctx.locale.toUpperCase()}`,
//
// NOTE: instead of using timezone from IP we use
// their last time zone set in a browser session
// (this is way more accurate and faster)
//
// here were some alternatives though during R&D:
// * <https://github.com/runk/node-maxmind>
// * <https://github.com/evansiroky/node-geo-tz>
// * <https://github.com/safing/mmdbmeld>
// * <https://github.com/sapics/ip-location-db>
//
timezone: ctx.state.session.user.timezone,
url: config.urls.web,
readonly: false,
synctoken: `${config.urls.web}/ns/sync-token/1`
});

logger.debug('getCalendar result', { calendar });

Expand Down Expand Up @@ -1030,6 +1031,11 @@ class CalDAV extends API {
user
});

if (!calendar)
throw Boom.methodNotAllowed(
ctx.translateError('CALENDAR_DOES_NOT_EXIST')
);

const update = {
name: comp.getFirstPropertyValue('name'),
prodId: comp.getFirstPropertyValue('prodid'),
Expand Down Expand Up @@ -1169,6 +1175,11 @@ class CalDAV extends API {
user
});

if (!calendar)
throw Boom.methodNotAllowed(
ctx.translateError('CALENDAR_DOES_NOT_EXIST')
);

return CalendarEvents.find(this, ctx.state.session, {
calendar: calendar._id
});
Expand All @@ -1194,6 +1205,11 @@ class CalDAV extends API {
user
});

if (!calendar)
throw Boom.methodNotAllowed(
ctx.translateError('CALENDAR_DOES_NOT_EXIST')
);

// TODO: incorporate database date query instead of this in-memory filtering
// TODO: we could do partial query for not recurring and b/w and then has recurring and after
const events = await CalendarEvents.find(this, ctx.state.session, {
Expand Down Expand Up @@ -1305,6 +1321,11 @@ class CalDAV extends API {
user
});

if (!calendar)
throw Boom.methodNotAllowed(
ctx.translateError('CALENDAR_DOES_NOT_EXIST')
);

const event = await CalendarEvents.findOne(this, ctx.state.session, {
eventId,
calendar: calendar._id
Expand Down Expand Up @@ -1332,6 +1353,11 @@ class CalDAV extends API {
user
});

if (!calendar)
throw Boom.methodNotAllowed(
ctx.translateError('CALENDAR_DOES_NOT_EXIST')
);

// check if there is an event with same calendar ID already
const exists = await CalendarEvents.findOne(this, ctx.state.session, {
eventId,
Expand Down Expand Up @@ -1454,6 +1480,11 @@ class CalDAV extends API {
user
});

if (!calendar)
throw Boom.methodNotAllowed(
ctx.translateError('CALENDAR_DOES_NOT_EXIST')
);

let e = await CalendarEvents.findOne(this, ctx.state.session, {
eventId,
calendar: calendar._id
Expand Down Expand Up @@ -1513,7 +1544,39 @@ class CalDAV extends API {
return e;
}

async deleteCalendar(ctx, { principalId, calendarId, user }) {
// , calendar
logger.debug('deleteCalendar', { principalId, calendarId, user });

const calendar = await this.getCalendar(ctx, {
calendarId,
principalId,
user
});

// delete all events for this calendar
try {
await CalendarEvents.deleteMany(this, ctx.state.session, {
calendar: calendar
? calendar._id
: new mongoose.Types.ObjectId(calendarId)
});
} catch (err) {
logger.error(err);
}

// delete the calendar itself
try {
await Calendars.deleteOne(this, ctx.state.session, {
_id: calendar ? calendarId : new mongoose.Types.ObjectId(calendarId)
});
} catch (err) {
logger.error(err);
}
}

async deleteEvent(ctx, { eventId, principalId, calendarId, user }) {
// , calendar
logger.debug('deleteEvent', { eventId, principalId, calendarId, user });

const calendar = await this.getCalendar(ctx, {
Expand All @@ -1522,6 +1585,11 @@ class CalDAV extends API {
user
});

if (!calendar)
throw Boom.methodNotAllowed(
ctx.translateError('CALENDAR_DOES_NOT_EXIST')
);

const event = await CalendarEvents.findOne(this, ctx.state.session, {
eventId,
calendar: calendar._id
Expand Down
1 change: 1 addition & 0 deletions config/phrases.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ module.exports = {
UBUNTU_INVALID_GROUP:
'You must be a member of a specific Launchpad group to get access. Supported groups include ~ubuntumembers, ~kubuntu-members, ~lubuntu-members, ~edubuntu-members, and ~ubuntustudio-core.',
CALENDAR: 'Calendar',
CALENDAR_ALREADY_EXISTS: 'Calendar already exists.',
CALENDAR_DOES_NOT_EXIST: 'Calendar does not exist.',
EVENT_ALREADY_EXISTS: 'Event ID already exists within the same calendar.',
EVENT_DOES_NOT_EXIST: 'Event does not exist.',
Expand Down
3 changes: 2 additions & 1 deletion locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -10319,5 +10319,6 @@
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"%s\"></span> from admins of the domain.": "الحصة المخصصة لـ <span class=\"notranslate\">%s</span> من <span class=\"notranslate\">%s</span> تتجاوز الحصة القصوى المخصصة لـ<span class=\"%s\"></span> من مسؤولي المجال.",
"Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "كانت البايتات غير صالحة، يجب أن تكون سلسلة مثل \"1 جيجابايت\" أو \"100 ميجابايت\".",
"alias quota": "حصة الاسم المستعار",
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"notranslate\">%s</span> from admins of the domain.": "تتجاوز الحصة المخصصة لـ <span class=\"notranslate\">%s</span> من <span class=\"notranslate\">%s</span> الحد الأقصى للحصة المخصصة لـ <span class=\"notranslate\">%s</span> من مسؤولي المجال."
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"notranslate\">%s</span> from admins of the domain.": "تتجاوز الحصة المخصصة لـ <span class=\"notranslate\">%s</span> من <span class=\"notranslate\">%s</span> الحد الأقصى للحصة المخصصة لـ <span class=\"notranslate\">%s</span> من مسؤولي المجال.",
"Calendar already exists.": "التقويم موجود بالفعل."
}
3 changes: 2 additions & 1 deletion locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -10319,5 +10319,6 @@
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"%s\"></span> from admins of the domain.": "Kvóta pro <span class=\"notranslate\">%s</span> z <span class=\"notranslate\">%s</span> překračuje maximální kvótu<span class=\"%s\"></span> od administrátorů domény.",
"Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Bajty byly neplatné, musí to být řetězec, například „1 GB“ nebo „100 MB“.",
"alias quota": "alias kvóta",
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"notranslate\">%s</span> from admins of the domain.": "Kvóta pro <span class=\"notranslate\">%s</span> z <span class=\"notranslate\">%s</span> překračuje maximální kvótu <span class=\"notranslate\">%s</span> od administrátorů domény."
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"notranslate\">%s</span> from admins of the domain.": "Kvóta pro <span class=\"notranslate\">%s</span> z <span class=\"notranslate\">%s</span> překračuje maximální kvótu <span class=\"notranslate\">%s</span> od administrátorů domény.",
"Calendar already exists.": "Kalendář již existuje."
}
3 changes: 2 additions & 1 deletion locales/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -7307,5 +7307,6 @@
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"%s\"></span> from admins of the domain.": "Kvoten for <span class=\"notranslate\">%s</span> af <span class=\"notranslate\">%s</span> overstiger den maksimale kvote på<span class=\"%s\"></span> fra domænets administratorer.",
"Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Bytes var ugyldige, skal være en streng såsom \"1 GB\" eller \"100 MB\".",
"alias quota": "alias kvote",
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"notranslate\">%s</span> from admins of the domain.": "Kvoten for <span class=\"notranslate\">%s</span> af <span class=\"notranslate\">%s</span> overstiger den maksimale kvote på <span class=\"notranslate\">%s</span> fra domænets administratorer."
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"notranslate\">%s</span> from admins of the domain.": "Kvoten for <span class=\"notranslate\">%s</span> af <span class=\"notranslate\">%s</span> overstiger den maksimale kvote på <span class=\"notranslate\">%s</span> fra domænets administratorer.",
"Calendar already exists.": "Kalenderen findes allerede."
}
3 changes: 2 additions & 1 deletion locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -9358,5 +9358,6 @@
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"%s\"></span> from admins of the domain.": "Das Kontingent für <span class=\"notranslate\">%s</span> von <span class=\"notranslate\">%s</span> überschreitet das maximale Kontingent von<span class=\"%s\"></span> von Admins der Domäne.",
"Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Die Bytes waren ungültig. Es muss sich um eine Zeichenfolge wie „1 GB“ oder „100 MB“ handeln.",
"alias quota": "Alias-Kontingent",
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"notranslate\">%s</span> from admins of the domain.": "Das Kontingent für <span class=\"notranslate\">%s</span> von <span class=\"notranslate\">%s</span> überschreitet das maximale Kontingent von <span class=\"notranslate\">%s</span> von Administratoren der Domäne."
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"notranslate\">%s</span> from admins of the domain.": "Das Kontingent für <span class=\"notranslate\">%s</span> von <span class=\"notranslate\">%s</span> überschreitet das maximale Kontingent von <span class=\"notranslate\">%s</span> von Administratoren der Domäne.",
"Calendar already exists.": "Kalender existiert bereits."
}
3 changes: 2 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -6699,5 +6699,6 @@
"Storage Max Quota": "Storage Max Quota",
"Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of <span class=\"notranslate\">%s</span>. Enter a human-friendly string such as \"1GB\" &ndash; note that we use <a href=\"https://github.com/visionmedia/bytes.js\" class=\"notranslate alert-link\" target=\"_blank\" rel=\"noopener noreferrer\">bytes</a> to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.": "Domain admins can update the storage quota for this alias. Leave blank and hit save to reset it to the current domain's maximum storage quota of <span class=\"notranslate\">%s</span>. Enter a human-friendly string such as \"1GB\" &ndash; note that we use <a href=\"https://github.com/visionmedia/bytes.js\" class=\"notranslate alert-link\" target=\"_blank\" rel=\"noopener noreferrer\">bytes</a> to parse the value to a Number. If you would like to update the max storage quota across all aliases for this domain, then go to the domain's Settings page.",
"Alias does not exist on the domain.": "Alias does not exist on the domain.",
"alias quota": "alias quota"
"alias quota": "alias quota",
"Calendar": "Calendar"
}
3 changes: 2 additions & 1 deletion locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -10317,5 +10317,6 @@
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"%s\"></span> from admins of the domain.": "La cuota para <span class=\"notranslate\">%s</span> de <span class=\"notranslate\">%s</span> excede la cuota máxima de<span class=\"%s\"></span> de los administradores del dominio.",
"Bytes were invalid, must be a string such as \"1 GB\" or \"100 MB\".": "Los bytes no son válidos, debe ser una cadena como \"1 GB\" o \"100 MB\".",
"alias quota": "alias cuota",
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"notranslate\">%s</span> from admins of the domain.": "La cuota de <span class=\"notranslate\">%s</span> de <span class=\"notranslate\">%s</span> excede la cuota máxima de <span class=\"notranslate\">%s</span> de administradores del dominio."
"The quota for <span class=\"notranslate\">%s</span> of <span class=\"notranslate\">%s</span> exceeds the maximum quota of <span class=\"notranslate\">%s</span> from admins of the domain.": "La cuota de <span class=\"notranslate\">%s</span> de <span class=\"notranslate\">%s</span> excede la cuota máxima de <span class=\"notranslate\">%s</span> de administradores del dominio.",
"Calendar already exists.": "El calendario ya existe."
}
Loading

0 comments on commit f46d6bc

Please sign in to comment.