1 /*
2  * This file is part of LibKGAPI library
3  *
4  * SPDX-FileCopyrightText: 2013 Daniel Vrátil <dvratil@redhat.com>
5  *
6  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7  */
8 
9 #include "calendarservice.h"
10 #include "calendar.h"
11 #include "event.h"
12 #include "reminder.h"
13 #include "utils.h"
14 #include "../debug.h"
15 
16 #include <KCalendarCore/Alarm>
17 #include <KCalendarCore/Event>
18 #include <KCalendarCore/Attendee>
19 #include <KCalendarCore/Person>
20 #include <KCalendarCore/Recurrence>
21 #include <KCalendarCore/RecurrenceRule>
22 #include <KCalendarCore/ICalFormat>
23 
24 #include <QJsonDocument>
25 #include <QUrlQuery>
26 #include <QTimeZone>
27 #include <QVariant>
28 #include <QNetworkRequest>
29 
30 #include <map>
31 #include <memory>
32 
33 namespace KGAPI2
34 {
35 
36 namespace CalendarService
37 {
38 
39 namespace Private
40 {
41 KCalendarCore::DateList parseRDate(const QString &rule);
42 
43 ObjectPtr JSONToCalendar(const QVariantMap &data);
44 ObjectPtr JSONToEvent(const QVariantMap &data, const QString &timezone = QString());
45 
46 /**
47  * Checks whether TZID is in Olson format and converts it to it if necessary
48  *
49  * This is mainly to handle crazy Microsoft TZIDs like
50  * "(GMT) Greenwich Mean Time/Dublin/Edinburgh/London", because Google only
51  * accepts TZIDs in Olson format ("Europe/London").
52  *
53  * It first tries to match the given \p tzid to all TZIDs in KTimeZones::zones().
54  * If it fails, it parses the \p event, looking for X-MICROSOFT-CDO-TZID
55  * property and than matches it to Olson-formatted TZID using a table.
56  *
57  * When the method fails to process the TZID, it returns the original \p tzid
58  * in hope, that Google will cope with it.
59  */
60 QString checkAndConverCDOTZID(const QString &tzid, const EventPtr& event);
61 
62 static const QUrl GoogleApisUrl(QStringLiteral("https://www.googleapis.com"));
63 static const QString CalendarListBasePath(QStringLiteral("/calendar/v3/users/me/calendarList"));
64 static const QString CalendarBasePath(QStringLiteral("/calendar/v3/calendars"));
65 }
66 
prepareRequest(const QUrl & url)67 QNetworkRequest prepareRequest(const QUrl &url)
68 {
69     QNetworkRequest request(url);
70     request.setRawHeader("GData-Version", CalendarService::APIVersion().toLatin1());
71 
72     return request;
73 }
74 
75 /************* URLS **************/
76 
fetchCalendarsUrl()77 QUrl fetchCalendarsUrl()
78 {
79     QUrl url(Private::GoogleApisUrl);
80     url.setPath(Private::CalendarListBasePath);
81     return url;
82 }
83 
fetchCalendarUrl(const QString & calendarID)84 QUrl fetchCalendarUrl(const QString& calendarID)
85 {
86     QUrl url(Private::GoogleApisUrl);
87     url.setPath(Private::CalendarListBasePath % QLatin1Char('/') % calendarID);
88     return url;
89 }
90 
updateCalendarUrl(const QString & calendarID)91 QUrl updateCalendarUrl(const QString &calendarID)
92 {
93     QUrl url(Private::GoogleApisUrl);
94     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID);
95     return url;
96 }
97 
createCalendarUrl()98 QUrl createCalendarUrl()
99 {
100     QUrl url(Private::GoogleApisUrl);
101     url.setPath(Private::CalendarBasePath);
102     return url;
103 }
104 
removeCalendarUrl(const QString & calendarID)105 QUrl removeCalendarUrl(const QString& calendarID)
106 {
107     QUrl url(Private::GoogleApisUrl);
108     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID);
109     return url;
110 }
111 
fetchEventsUrl(const QString & calendarID)112 QUrl fetchEventsUrl(const QString& calendarID)
113 {
114     QUrl url(Private::GoogleApisUrl);
115     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1String("/events"));
116     return url;
117 }
118 
fetchEventUrl(const QString & calendarID,const QString & eventID)119 QUrl fetchEventUrl(const QString& calendarID, const QString& eventID)
120 {
121     QUrl url(Private::GoogleApisUrl);
122     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1String("/events/") % eventID);
123     return url;
124 }
125 
126 namespace {
127 
sendUpdatesPolicyToString(SendUpdatesPolicy policy)128 QString sendUpdatesPolicyToString(SendUpdatesPolicy policy)
129 {
130     switch (policy) {
131     case SendUpdatesPolicy::All:
132         return QStringLiteral("all");
133     case SendUpdatesPolicy::ExternalOnly:
134         return QStringLiteral("externalOnly");
135     case SendUpdatesPolicy::None:
136         return QStringLiteral("none");
137     }
138     Q_UNREACHABLE();
139 }
140 
141 static const QString sendUpatesQueryParam = QStringLiteral("sendUpdates");
142 static const QString destinationQueryParam = QStringLiteral("destination");
143 }
144 
updateEventUrl(const QString & calendarID,const QString & eventID,SendUpdatesPolicy updatePolicy)145 QUrl updateEventUrl(const QString& calendarID, const QString& eventID, SendUpdatesPolicy updatePolicy)
146 {
147     QUrl url(Private::GoogleApisUrl);
148     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1String("/events/") % eventID);
149     QUrlQuery query(url);
150     query.addQueryItem(sendUpatesQueryParam, sendUpdatesPolicyToString(updatePolicy));
151     url.setQuery(query);
152     return url;
153 }
154 
createEventUrl(const QString & calendarID,SendUpdatesPolicy updatePolicy)155 QUrl createEventUrl(const QString& calendarID, SendUpdatesPolicy updatePolicy)
156 {
157     QUrl url(Private::GoogleApisUrl);
158     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1String("/events"));
159     QUrlQuery query(url);
160     query.addQueryItem(sendUpatesQueryParam, sendUpdatesPolicyToString(updatePolicy));
161     url.setQuery(query);
162     return url;
163 }
164 
importEventUrl(const QString & calendarID,SendUpdatesPolicy updatePolicy)165 QUrl importEventUrl(const QString& calendarID, SendUpdatesPolicy updatePolicy)
166 {
167     QUrl url(Private::GoogleApisUrl);
168     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1String("/events") % QLatin1String("/import"));
169     QUrlQuery query(url);
170     query.addQueryItem(sendUpatesQueryParam, sendUpdatesPolicyToString(updatePolicy));
171     url.setQuery(query);
172     return url;
173 }
174 
removeEventUrl(const QString & calendarID,const QString & eventID)175 QUrl removeEventUrl(const QString& calendarID, const QString& eventID)
176 {
177     QUrl url(Private::GoogleApisUrl);
178     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1String("/events/") % eventID);
179     return url;
180 }
181 
moveEventUrl(const QString & sourceCalendar,const QString & destCalendar,const QString & eventID)182 QUrl moveEventUrl(const QString& sourceCalendar, const QString& destCalendar, const QString& eventID)
183 {
184     QUrl url(Private::GoogleApisUrl);
185     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % sourceCalendar % QLatin1String("/events/") % eventID % QLatin1String("/move"));
186     QUrlQuery query(url);
187     query.addQueryItem(destinationQueryParam, destCalendar);
188     url.setQuery(query);
189     return url;
190 }
191 
freeBusyQueryUrl()192 QUrl freeBusyQueryUrl()
193 {
194     QUrl url(Private::GoogleApisUrl);
195     url.setPath(QStringLiteral("/calendar/v3/freeBusy"));
196     return url;
197 }
198 
199 namespace {
200 
201 static const auto kindParam = QStringLiteral("kind");
202 static const auto idParam = QStringLiteral("id");
203 static const auto etagParam = QStringLiteral("etag");
204 
205 static const auto nextSyncTokenParam = QStringLiteral("nextSyncToken");
206 static const auto nextPageTokenParam = QStringLiteral("nextPageToken");
207 static const auto pageTokenParam = QStringLiteral("pageToken");
208 static const auto itemsParam = QStringLiteral("items");
209 
210 static const auto calendarSummaryParam = QStringLiteral("summary");
211 static const auto calendarDescriptionParam = QStringLiteral("description");
212 static const auto calendarLocationParam = QStringLiteral("location");
213 static const auto calendarTimezoneParam = QStringLiteral("timeZone");
214 static const auto calendarBackgroundColorParam = QStringLiteral("backgroundColor");
215 static const auto calendarForegroundColorParam = QStringLiteral("foregroundColor");
216 static const auto calendarAccessRoleParam = QStringLiteral("accessRole");
217 static const auto calendarDefaultRemindersParam = QStringLiteral("defaultReminders");
218 static const auto reminderMethodParam = QStringLiteral("method");
219 static const auto reminderMinutesParam = QStringLiteral("minutes");
220 
221 static const auto eventiCalUIDParam = QStringLiteral("iCalUID");
222 static const auto eventStatusParam = QStringLiteral("status");
223 static const auto eventCreatedParam = QStringLiteral("created");
224 static const auto eventUpdatedParam = QStringLiteral("updated");
225 static const auto eventSummaryParam = QStringLiteral("summary");
226 static const auto eventDescriptionParam = QStringLiteral("description");
227 static const auto eventLocationParam = QStringLiteral("location");
228 static const auto eventStartPram = QStringLiteral("start");
229 static const auto eventEndParam = QStringLiteral("end");
230 static const auto eventOriginalStartTimeParam = QStringLiteral("originalStartTime");
231 static const auto eventTransparencyParam = QStringLiteral("transparency");
232 static const auto eventOrganizerParam = QStringLiteral("organizer");
233 static const auto eventAttendeesParam = QStringLiteral("attendees");
234 static const auto eventRecurrenceParam = QStringLiteral("recurrence");
235 static const auto eventRemindersParam = QStringLiteral("reminders");
236 static const auto eventExtendedPropertiesParam = QStringLiteral("extendedProperties");
237 static const auto eventRecurringEventIdParam = QStringLiteral("recurringEventId");
238 
239 static const auto attendeeDisplayNameParam = QStringLiteral("displayName");
240 static const auto attendeeEmailParam = QStringLiteral("email");
241 static const auto attendeeResponseStatusParam = QStringLiteral("responseStatus");
242 static const auto attendeeOptionalParam = QStringLiteral("optional");
243 
244 static const auto organizerDisplayNameParam = QStringLiteral("displayName");
245 static const auto organizerEmailParam = QStringLiteral("email");
246 
247 static const auto reminderUseDefaultParam = QStringLiteral("useDefault");
248 static const auto reminderOverridesParam = QStringLiteral("overrides");
249 
250 static const auto propertyPrivateParam = QStringLiteral("private");
251 static const auto propertySharedParam = QStringLiteral("shared");
252 
253 static const auto dateParam = QStringLiteral("date");
254 static const auto dateTimeParam = QStringLiteral("dateTime");
255 static const auto timeZoneParam = QStringLiteral("timeZone");
256 
257 
258 static const auto calendarListKind = QLatin1String("calendar#calendarList");
259 static const auto calendarListEntryKind = QLatin1String("calendar#calendarListEntry");
260 static const auto calendarKind = QLatin1String("calendar#calendar");
261 static const auto eventKind = QLatin1String("calendar#event");
262 static const auto eventsKind = QLatin1String("calendar#events");
263 
264 static const auto writerAccessRole = QLatin1String("writer");
265 static const auto ownerAccessRole = QLatin1String("owner");
266 static const auto emailMethod = QLatin1String("email");
267 static const auto popupMethod = QLatin1String("popup");
268 
269 static const auto confirmedStatus = QLatin1String("confirmed");
270 static const auto canceledStatus = QLatin1String("cancelled");
271 static const auto tentativeStatus = QLatin1String("tentative");
272 static const auto acceptedStatus = QLatin1String("accepted");
273 static const auto needsActionStatus = QLatin1String("needsAction");
274 static const auto transparentTransparency = QLatin1String("transparent");
275 static const auto opaqueTransparency = QLatin1String("opaque");
276 static const auto declinedStatus = QLatin1String("declined");
277 static const auto categoriesProperty = QLatin1String("categories");
278 
279 }
280 
APIVersion()281 QString APIVersion()
282 {
283     return QStringLiteral("3");
284 }
285 
JSONToCalendar(const QByteArray & jsonData)286 CalendarPtr JSONToCalendar(const QByteArray& jsonData)
287 {
288     const auto document = QJsonDocument::fromJson(jsonData);
289     const auto calendar = document.toVariant().toMap();
290 
291     if (calendar.value(kindParam).toString() != calendarListEntryKind && calendar.value(kindParam).toString() != calendarKind) {
292         return CalendarPtr();
293     }
294 
295     return Private::JSONToCalendar(calendar).staticCast<Calendar>();
296 }
297 
JSONToCalendar(const QVariantMap & data)298 ObjectPtr Private::JSONToCalendar(const QVariantMap& data)
299 {
300     auto calendar = CalendarPtr::create();
301 
302     const auto id = QUrl::fromPercentEncoding(data.value(idParam).toByteArray());
303     calendar->setUid(id);
304     calendar->setEtag(data.value(etagParam).toString());
305     calendar->setTitle(data.value(calendarSummaryParam).toString());
306     calendar->setDetails(data.value(calendarDescriptionParam).toString());
307     calendar->setLocation(data.value(calendarLocationParam).toString());
308     calendar->setTimezone(data.value(calendarTimezoneParam).toString());
309     calendar->setBackgroundColor(QColor(data.value(calendarBackgroundColorParam).toString()));
310     calendar->setForegroundColor(QColor(data.value(calendarForegroundColorParam).toString()));
311 
312     if ((data.value(calendarAccessRoleParam).toString() == writerAccessRole) ||
313             (data.value(calendarAccessRoleParam).toString() == ownerAccessRole)) {
314         calendar->setEditable(true);
315     } else {
316         calendar->setEditable(false);
317     }
318 
319     const auto reminders = data.value(calendarDefaultRemindersParam).toList();
320     for (const auto &r : reminders) {
321         const auto reminder = r.toMap();
322 
323         auto rem = ReminderPtr::create();
324         if (reminder.value(reminderMethodParam).toString() == emailMethod) {
325             rem->setType(KCalendarCore::Alarm::Email);
326         } else if (reminder.value(reminderMethodParam).toString() == popupMethod) {
327             rem->setType(KCalendarCore::Alarm::Display);
328         } else {
329             rem->setType(KCalendarCore::Alarm::Invalid);
330         }
331 
332         rem->setStartOffset(KCalendarCore::Duration(reminder.value(reminderMinutesParam).toInt() * (-60)));
333 
334         calendar->addDefaultReminer(rem);
335     }
336 
337     return calendar.dynamicCast<Object>();
338 }
339 
calendarToJSON(const CalendarPtr & calendar)340 QByteArray calendarToJSON(const CalendarPtr& calendar)
341 {
342     QVariantMap entry;
343 
344     if (!calendar->uid().isEmpty()) {
345         entry.insert(idParam, calendar->uid());
346     }
347 
348     entry.insert(calendarSummaryParam, calendar->title());
349     entry.insert(calendarDescriptionParam, calendar->details());
350     entry.insert(calendarLocationParam, calendar->location());
351     if (!calendar->timezone().isEmpty()) {
352         entry.insert(calendarTimezoneParam, calendar->timezone());
353     }
354 
355     const auto document = QJsonDocument::fromVariant(entry);
356     return document.toJson(QJsonDocument::Compact);
357 }
358 
359 
parseCalendarJSONFeed(const QByteArray & jsonFeed,FeedData & feedData)360 ObjectsList parseCalendarJSONFeed(const QByteArray& jsonFeed, FeedData& feedData)
361 {
362     const auto document = QJsonDocument::fromJson(jsonFeed);
363     const auto data = document.toVariant().toMap();
364 
365     ObjectsList list;
366 
367     if (data.value(kindParam).toString() == calendarListKind) {
368         if (data.contains(nextPageTokenParam)) {
369             feedData.nextPageUrl = fetchCalendarsUrl();
370             QUrlQuery query(feedData.nextPageUrl);
371             query.addQueryItem(pageTokenParam, data.value(nextPageTokenParam).toString());
372             feedData.nextPageUrl.setQuery(query);
373         }
374     } else {
375         return {};
376     }
377 
378     const auto items = data.value(itemsParam).toList();
379     list.reserve(items.size());
380     for (const auto &i : items) {
381         list.push_back(Private::JSONToCalendar(i.toMap()));
382     }
383 
384     return list;
385 }
386 
JSONToEvent(const QByteArray & jsonData)387 EventPtr JSONToEvent(const QByteArray& jsonData)
388 {
389     QJsonParseError error;
390     QJsonDocument document = QJsonDocument::fromJson(jsonData, &error);
391     if (error.error != QJsonParseError::NoError) {
392         qCWarning(KGAPIDebug) << "Error parsing event JSON: " << error.errorString();
393     }
394     QVariantMap data = document.toVariant().toMap();
395     if (data.value(kindParam).toString() != eventKind) {
396         return EventPtr();
397     }
398 
399     return Private::JSONToEvent(data).staticCast<Event>();
400 }
401 
402 namespace {
403 
404 struct ParsedDt {
405     QDateTime dt;
406     bool isAllDay;
407 };
408 
parseDt(const QVariantMap & data,const QString & timezone,bool isDtEnd)409 ParsedDt parseDt(const QVariantMap &data, const QString &timezone, bool isDtEnd)
410 {
411     if (data.contains(dateParam)) {
412         auto dt = QDateTime::fromString(data.value(dateParam).toString(), Qt::ISODate);
413         if (isDtEnd) {
414             // Google reports all-day events to end on the next day, e.g. a
415             // Monday all-day event will be reporting as starting on Monday and
416             // ending on Tuesday, while KCalendarCore/iCal uses the same day for
417             // dtEnd, so adjust the end date here.
418             dt = dt.addDays(-1);
419         }
420         return {dt, true};
421     } else if (data.contains(dateTimeParam)) {
422         auto dt = Utils::rfc3339DateFromString(data.value(dateTimeParam).toString());
423         // If there's a timezone specified in the "start" entity, then use it
424         if (data.contains(timeZoneParam)) {
425             const QTimeZone tz = QTimeZone(data.value(timeZoneParam).toString().toUtf8());
426             if (tz.isValid()) {
427                 dt = dt.toTimeZone(tz);
428             } else {
429                 qCWarning(KGAPIDebug) << "Invalid timezone" << data.value(timeZoneParam).toString();
430             }
431 
432             // Otherwise try to fallback to calendar-wide timezone
433         } else if (!timezone.isEmpty()) {
434             const QTimeZone tz(timezone.toUtf8());
435             if (tz.isValid()) {
436                 dt.setTimeZone(tz);
437             } else {
438                 qCWarning(KGAPIDebug) << "Invalid timezone" << timezone;
439             }
440         }
441         return {dt, false};
442     } else {
443         return {{}, false};
444     }
445 }
446 
setEventCategories(EventPtr & event,const QVariantMap & properties)447 void setEventCategories(EventPtr &event, const QVariantMap &properties)
448 {
449     for (auto iter = properties.cbegin(), end = properties.cend(); iter != end; ++iter) {
450         if (iter.key() == categoriesProperty) {
451             event->setCategories(iter.value().toString());
452         }
453     }
454 }
455 
456 } // namespace
457 
JSONToEvent(const QVariantMap & data,const QString & timezone)458 ObjectPtr Private::JSONToEvent(const QVariantMap& data, const QString &timezone)
459 {
460     auto event = EventPtr::create();
461 
462     event->setId(data.value(idParam).toString());
463     event->setUid(data.value(eventiCalUIDParam).toString());
464     event->setEtag(data.value(etagParam).toString());
465 
466     if (data.value(eventStatusParam).toString() == confirmedStatus) {
467         event->setStatus(KCalendarCore::Incidence::StatusConfirmed);
468     } else if (data.value(eventStatusParam).toString() == canceledStatus) {
469         event->setStatus(KCalendarCore::Incidence::StatusCanceled);
470         event->setDeleted(true);
471     } else if (data.value(eventStatusParam).toString() == tentativeStatus) {
472         event->setStatus(KCalendarCore::Incidence::StatusTentative);
473     } else {
474         event->setStatus(KCalendarCore::Incidence::StatusNone);
475     }
476 
477     event->setCreated(Utils::rfc3339DateFromString(data.value(eventCreatedParam).toString()));
478     event->setLastModified(Utils::rfc3339DateFromString(data.value(eventUpdatedParam).toString()));
479     event->setSummary(data.value(eventSummaryParam).toString());
480     event->setDescription(data.value(eventDescriptionParam).toString());
481     event->setLocation(data.value(eventLocationParam).toString());
482 
483     const auto dtStart = parseDt(data.value(eventStartPram).toMap(), timezone, false);
484     event->setDtStart(dtStart.dt);
485     event->setAllDay(dtStart.isAllDay);
486 
487     const auto dtEnd = parseDt(data.value(eventEndParam).toMap(), timezone, true);
488     event->setDtEnd(dtEnd.dt);
489 
490     if (data.contains(eventOriginalStartTimeParam)) {
491         const auto recurrenceId = parseDt(data.value(eventOriginalStartTimeParam).toMap(), timezone, false);
492         event->setRecurrenceId(recurrenceId.dt);
493     }
494 
495     if (data.value(eventTransparencyParam).toString() == transparentTransparency) {
496         event->setTransparency(Event::Transparent);
497     } else { /* Assume opaque as default transparency */
498         event->setTransparency(Event::Opaque);
499     }
500 
501     const auto attendees = data.value(eventAttendeesParam).toList();
502     for (const auto &a : attendees) {
503         const auto att = a.toMap();
504         KCalendarCore::Attendee attendee(
505                         att.value(attendeeDisplayNameParam).toString(),
506                         att.value(attendeeEmailParam).toString());
507         const auto responseStatus = att.value(attendeeResponseStatusParam).toString();
508         if (responseStatus == acceptedStatus) {
509             attendee.setStatus(KCalendarCore::Attendee::Accepted);
510         } else if (responseStatus == declinedStatus) {
511             attendee.setStatus(KCalendarCore::Attendee::Declined);
512         } else if (responseStatus == tentativeStatus) {
513             attendee.setStatus(KCalendarCore::Attendee::Tentative);
514         } else {
515             attendee.setStatus(KCalendarCore::Attendee::NeedsAction);
516         }
517 
518         if (att.value(attendeeOptionalParam).toBool()) {
519             attendee.setRole(KCalendarCore::Attendee::OptParticipant);
520         }
521         const auto uid = att.value(idParam).toString();
522         if (!uid.isEmpty()) {
523             attendee.setUid(uid);
524         } else {
525             // Set some UID, just so that the results are reproducible
526             attendee.setUid(QString::number(qHash(attendee.email())));
527         }
528         event->addAttendee(attendee, true);
529     }
530 
531     /* According to RFC, only events with attendees can have an organizer.
532      * Google seems to ignore it, so we must take care of it here */
533     if (event->attendeeCount() > 0) {
534         KCalendarCore::Person organizer;
535         const auto organizerData = data.value(eventOrganizerParam).toMap();
536         organizer.setName(organizerData.value(organizerDisplayNameParam).toString());
537         organizer.setEmail(organizerData.value(organizerEmailParam).toString());
538         event->setOrganizer(organizer);
539     }
540 
541     const QStringList recrs = data.value(eventRecurrenceParam).toStringList();
542     for (const QString & rec : recrs) {
543         KCalendarCore::ICalFormat format;
544         const QStringView recView(rec);
545         if (recView.left(5) == QLatin1String("RRULE")) {
546             auto recurrenceRule = std::make_unique<KCalendarCore::RecurrenceRule>();
547             const auto ok = format.fromString(recurrenceRule.get(), rec.mid(6)); Q_UNUSED(ok)
548             recurrenceRule->setRRule(rec);
549             event->recurrence()->addRRule(recurrenceRule.release());
550         } else if (recView.left(6) == QLatin1String("EXRULE")) {
551             auto recurrenceRule = std::make_unique<KCalendarCore::RecurrenceRule>();
552             const auto ok = format.fromString(recurrenceRule.get(), rec.mid(7)); Q_UNUSED(ok)
553             recurrenceRule->setRRule(rec);
554             event->recurrence()->addExRule(recurrenceRule.release());
555         } else if (recView.left(6) == QLatin1String("EXDATE")) {
556             KCalendarCore::DateList exdates = Private::parseRDate(rec);
557             event->recurrence()->setExDates(exdates);
558         } else if (recView.left(5) == QLatin1String("RDATE")) {
559             KCalendarCore::DateList rdates = Private::parseRDate(rec);
560             event->recurrence()->setRDates(rdates);
561         }
562     }
563 
564     const auto reminders = data.value(eventRemindersParam).toMap();
565     if (reminders.contains(reminderUseDefaultParam) && reminders.value(reminderUseDefaultParam).toBool()) {
566         event->setUseDefaultReminders(true);
567     } else {
568         event->setUseDefaultReminders(false);
569     }
570 
571     const auto overrides = reminders.value(reminderOverridesParam).toList();
572     for (const auto &r : overrides) {
573         const auto reminderOverride = r.toMap();
574         auto alarm = KCalendarCore::Alarm::Ptr::create(static_cast<KCalendarCore::Incidence*>(event.data()));
575         alarm->setTime(event->dtStart());
576 
577         if (reminderOverride.value(reminderMethodParam).toString() == popupMethod) {
578             alarm->setType(KCalendarCore::Alarm::Display);
579         } else if (reminderOverride.value(reminderMethodParam).toString() == emailMethod) {
580             alarm->setType(KCalendarCore::Alarm::Email);
581         } else {
582             alarm->setType(KCalendarCore::Alarm::Invalid);
583             continue;
584         }
585 
586         alarm->setStartOffset(KCalendarCore::Duration(reminderOverride.value(reminderMinutesParam).toInt() * (-60)));
587         alarm->setEnabled(true);
588         event->addAlarm(alarm);
589     }
590 
591     const auto extendedProperties = data.value(eventExtendedPropertiesParam).toMap();
592     setEventCategories(event, extendedProperties.value(propertyPrivateParam).toMap());
593     setEventCategories(event, extendedProperties.value(propertySharedParam).toMap());
594 
595     return event.dynamicCast<Object>();
596 }
597 
598 namespace {
599 
600 enum class SerializeDtFlag {
601     AllDay =        1 << 0,
602     IsDtEnd =       1 << 1,
603     HasRecurrence = 1 << 2
604 };
605 using SerializeDtFlags = QFlags<SerializeDtFlag>;
606 
serializeDt(const EventPtr & event,const QDateTime & dt,SerializeDtFlags flags)607 QVariantMap serializeDt(const EventPtr &event, const QDateTime &dt, SerializeDtFlags flags)
608 {
609     QVariantMap rv;
610     if (flags & SerializeDtFlag::AllDay) {
611         /* For Google, all-day events starts on Monday and ends on Tuesday,
612          * while in KDE, it both starts and ends on Monday. */
613         const auto adjusted = dt.addDays(flags & SerializeDtFlag::IsDtEnd ? 1 : 0);
614         rv.insert(dateParam, adjusted.toString(QStringLiteral("yyyy-MM-dd")));
615     } else {
616         rv.insert(dateTimeParam, Utils::rfc3339DateToString(dt));
617         QString tzEnd = QString::fromUtf8(dt.timeZone().id());
618         if (flags & SerializeDtFlag::HasRecurrence && tzEnd.isEmpty()) {
619             tzEnd = QString::fromUtf8(QTimeZone::utc().id());
620         }
621         if (!tzEnd.isEmpty()) {
622             rv.insert(timeZoneParam, Private::checkAndConverCDOTZID(tzEnd, event));
623         }
624     }
625 
626     return rv;
627 }
628 
629 } // namespace
630 
eventToJSON(const EventPtr & event,EventSerializeFlags flags)631 QByteArray eventToJSON(const EventPtr& event, EventSerializeFlags flags)
632 {
633     QVariantMap data;
634 
635     data.insert(kindParam, eventKind);
636 
637     if (!(flags & EventSerializeFlag::NoID)) {
638         data.insert(idParam, event->id());
639     }
640 
641     data.insert(eventiCalUIDParam, event->uid());
642 
643     if (event->status() == KCalendarCore::Incidence::StatusConfirmed) {
644         data.insert(eventStatusParam, confirmedStatus);
645     } else if (event->status() == KCalendarCore::Incidence::StatusCanceled) {
646         data.insert(eventStatusParam, canceledStatus);
647     } else if (event->status() == KCalendarCore::Incidence::StatusTentative) {
648         data.insert(eventStatusParam, tentativeStatus);
649     }
650 
651     data.insert(eventSummaryParam, event->summary());
652     data.insert(eventDescriptionParam, event->description());
653     data.insert(eventLocationParam, event->location());
654 
655     QVariantList recurrence;
656     KCalendarCore::ICalFormat format;
657     const auto exRules = event->recurrence()->exRules();
658     const auto rRules = event->recurrence()->rRules();
659     recurrence.reserve(rRules.size() + rRules.size() + 2);
660     for (KCalendarCore::RecurrenceRule *rRule : rRules) {
661         recurrence.push_back(format.toString(rRule).remove(QStringLiteral("\r\n")));
662     }
663     for (KCalendarCore::RecurrenceRule *rRule : exRules) {
664         recurrence.push_back(format.toString(rRule).remove(QStringLiteral("\r\n")));
665     }
666 
667     QStringList dates;
668     const auto rDates = event->recurrence()->rDates();
669     dates.reserve(rDates.size());
670     for (const auto &rDate : rDates) {
671         dates.push_back(rDate.toString(QStringLiteral("yyyyMMdd")));
672     }
673 
674     if (!dates.isEmpty()) {
675         recurrence.push_back(QString(QStringLiteral("RDATE;VALUE=DATA:") + dates.join(QLatin1Char(','))));
676     }
677 
678     dates.clear();
679     const auto exDates = event->recurrence()->exDates();
680     dates.reserve(exDates.size());
681     for (const auto &exDate : exDates) {
682         dates.push_back(exDate.toString(QStringLiteral("yyyyMMdd")));
683     }
684 
685     if (!dates.isEmpty()) {
686         recurrence.push_back(QString(QStringLiteral("EXDATE;VALUE=DATE:") + dates.join(QLatin1Char(','))));
687     }
688 
689     if (!recurrence.isEmpty()) {
690         data.insert(eventRecurrenceParam, recurrence);
691     }
692 
693     SerializeDtFlags dtFlags;
694     if (event->allDay()) {
695         dtFlags |= SerializeDtFlag::AllDay;
696     }
697     if (!recurrence.isEmpty()) {
698         dtFlags |= SerializeDtFlag::HasRecurrence;
699     }
700 
701     data.insert(eventStartPram, serializeDt(event, event->dtStart(), dtFlags));
702     data.insert(eventEndParam, serializeDt(event, event->dtEnd(), dtFlags | SerializeDtFlag::IsDtEnd));
703 
704     if (event->hasRecurrenceId()) {
705         data.insert(eventOrganizerParam, serializeDt(event, event->recurrenceId(), dtFlags));
706         data.insert(eventRecurringEventIdParam, event->id());
707     }
708 
709     if (event->transparency() == Event::Transparent) {
710         data.insert(eventTransparencyParam, transparentTransparency);
711     } else {
712         data.insert(eventTransparencyParam, opaqueTransparency);
713     }
714 
715     QVariantList atts;
716     const auto attendees = event->attendees();
717     for (const auto &attee : attendees) {
718         QVariantMap att{{attendeeDisplayNameParam, attee.name()},
719                         {attendeeEmailParam, attee.email()}};
720 
721         if (attee.status() == KCalendarCore::Attendee::Accepted) {
722             att.insert(attendeeResponseStatusParam, acceptedStatus);
723         } else if (attee.status() == KCalendarCore::Attendee::Declined) {
724             att.insert(attendeeResponseStatusParam, declinedStatus);
725         } else if (attee.status() == KCalendarCore::Attendee::Tentative) {
726             att.insert(attendeeResponseStatusParam, tentativeStatus);
727         } else {
728             att.insert(attendeeResponseStatusParam, needsActionStatus);
729         }
730 
731         if (attee.role() == KCalendarCore::Attendee::OptParticipant) {
732             att.insert(attendeeOptionalParam, true);
733         }
734         if (!attee.uid().isEmpty()) {
735             att.insert(idParam, attee.uid());
736         }
737         atts.append(att);
738     }
739 
740     if (!atts.isEmpty()) {
741         data.insert(eventAttendeesParam, atts);
742 
743         /* According to RFC, event without attendees should not have
744          * any organizer. */
745         const auto organizer = event->organizer();
746         if (!organizer.isEmpty()) {
747             data.insert(eventOrganizerParam,
748                         QVariantMap{{organizerDisplayNameParam, organizer.fullName()},
749                                     {organizerEmailParam, organizer.email()}});
750         }
751     }
752 
753     QVariantList overrides;
754     const auto alarms = event->alarms();
755     for (const auto &alarm : alarms) {
756         QVariantMap reminderOverride;
757         if (alarm->type() == KCalendarCore::Alarm::Display) {
758             reminderOverride.insert(reminderMethodParam, popupMethod);
759         } else if (alarm->type() == KCalendarCore::Alarm::Email) {
760             reminderOverride.insert(reminderMethodParam, emailMethod);
761         } else {
762             continue;
763         }
764         reminderOverride.insert(reminderMinutesParam, (int)(alarm->startOffset().asSeconds() / -60));
765 
766         overrides.push_back(reminderOverride);
767     }
768 
769     data.insert(eventRemindersParam,
770                 QVariantMap{{reminderUseDefaultParam, false},
771                             {reminderOverridesParam, overrides}});
772 
773     if (!event->categories().isEmpty()) {
774         data.insert(eventExtendedPropertiesParam,
775                     QVariantMap{{propertySharedParam, QVariantMap{{categoriesProperty, event->categoriesStr()}}}});
776     }
777 
778     /* TODO: Implement support for additional features:
779      * https://developers.google.com/gdata/docs/2.0/elements?csw=1
780      */
781 
782     const auto document = QJsonDocument::fromVariant(data);
783     return document.toJson(QJsonDocument::Compact);
784 }
785 
parseEventJSONFeed(const QByteArray & jsonFeed,FeedData & feedData)786 ObjectsList parseEventJSONFeed(const QByteArray& jsonFeed, FeedData& feedData)
787 {
788     const auto document = QJsonDocument::fromJson(jsonFeed);
789     const auto data = document.toVariant().toMap();
790 
791     QString timezone;
792     if (data.value(kindParam) == eventsKind) {
793         if (data.contains(nextPageTokenParam)) {
794             QString calendarId = feedData.requestUrl.toString().remove(QStringLiteral("https://www.googleapis.com/calendar/v3/calendars/"));
795             calendarId = calendarId.left(calendarId.indexOf(QLatin1Char('/')));
796             feedData.nextPageUrl = feedData.requestUrl;
797             // replace the old pageToken with a new one
798             QUrlQuery query(feedData.nextPageUrl);
799             query.removeQueryItem(pageTokenParam);
800             query.addQueryItem(pageTokenParam, data.value(nextPageTokenParam).toString());
801             feedData.nextPageUrl.setQuery(query);
802         }
803         if (data.contains(timeZoneParam)) {
804             // This should always be in Olson format
805             timezone = data.value(timeZoneParam).toString();
806         }
807         if (data.contains(nextSyncTokenParam)) {
808             feedData.syncToken = data[nextSyncTokenParam].toString();
809         }
810     } else {
811         return {};
812     }
813 
814     ObjectsList list;
815     const auto items = data.value(itemsParam).toList();
816     list.reserve(items.size());
817     for (const auto &i : items) {
818         list.push_back(Private::JSONToEvent(i.toMap(), timezone));
819     }
820 
821     return list;
822 }
823 
824 /******************************** PRIVATE ***************************************/
825 
parseRDate(const QString & rule)826 KCalendarCore::DateList Private::parseRDate(const QString& rule)
827 {
828     KCalendarCore::DateList list;
829     QStringRef value;
830     QTimeZone tz;
831 
832     const auto left = rule.leftRef(rule.indexOf(QLatin1Char(':')));
833     const auto params = left.split(QLatin1Char(';'));
834     for (const auto &param : params) {
835         if (param.startsWith(QLatin1String("VALUE"))) {
836             value = param.mid(param.indexOf(QLatin1Char('=')) + 1);
837         } else if (param.startsWith(QLatin1String("TZID"))) {
838             auto _name = param.mid(param.indexOf(QLatin1Char('=')) + 1);
839             tz = QTimeZone(_name.toUtf8());
840         }
841     }
842 
843     const auto datesStr = rule.midRef(rule.lastIndexOf(QLatin1Char(':')) + 1);
844     const auto dates = datesStr.split(QLatin1Char(','));
845     for (const auto &date : dates) {
846         QDate dt;
847 
848         if (value == QLatin1String("DATE")) {
849             dt = QDate::fromString(date.toString(), QStringLiteral("yyyyMMdd"));
850         } else if (value == QLatin1String("PERIOD")) {
851             const auto start = date.left(date.indexOf(QLatin1Char('/')));
852             QDateTime kdt = Utils::rfc3339DateFromString(start.toString());
853             if (tz.isValid()) {
854                 kdt.setTimeZone(tz);
855             }
856 
857             dt = kdt.date();
858         } else {
859             QDateTime kdt = Utils::rfc3339DateFromString(date.toString());
860             if (tz.isValid()) {
861                 kdt.setTimeZone(tz);
862             }
863 
864             dt = kdt.date();
865         }
866 
867         list.push_back(dt);
868     }
869 
870     return list;
871 }
872 
873 namespace {
874 
875 /* Based on "Time Zone to CdoTimeZoneId Map"
876  * https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2007/aa563018(v=exchg.80)
877  *
878  * The mapping is not exact, since the CdoTimeZoneId usually refers to a
879  * region of multiple countries, so I always picked one of the countries
880  * in the specified region and used it's TZID.
881  */
882 static const std::map<int, QLatin1String> MSCDOTZIDTable = {
883     {0,  QLatin1String("UTC")},
884     {1,  QLatin1String("Europe/London")},             /* GMT Greenwich Mean Time; Dublin, Edinburgh, London */
885     /* Seriously? *sigh* Let's handle these two in checkAndConvertCDOTZID() */
886     //{2, QLatin1String("Europe/Lisbon")},              /* GMT Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London */
887     //{2, QLatin1String("Europe/Sarajevo")},            /* GMT+01:00 Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb */
888     {3,  QLatin1String("Europe/Paris")},              /* GMT+01:00 Paris, Madrid, Brussels, Copenhagen */
889     {4,  QLatin1String("Europe/Berlin")},             /* GMT+01:00 Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna */
890     {5,  QLatin1String("Europe/Bucharest")},          /* GMT+02:00 Bucharest */
891     {6,  QLatin1String("Europe/Prague")},             /* GMT+01:00 Prague, Central Europe */
892     {7,  QLatin1String("Europe/Athens")},             /* GMT+02:00 Athens, Istanbul, Minsk */
893     {8,  QLatin1String("America/Brazil")},            /* GMT-03:00 Brasilia */
894     {9,  QLatin1String("America/Halifax")},           /* GMT-04:00 Atlantic time (Canada) */
895     {10, QLatin1String("America/New_York")},          /* GMT-05:00 Eastern Time (US & Canada) */
896     {11, QLatin1String("America/Chicago")},           /* GMT-06:00 Central Time (US & Canada) */
897     {12, QLatin1String("America/Denver")},            /* GMT-07:00 Mountain Time (US & Canada) */
898     {13, QLatin1String("America/Los_Angeles")},       /* GMT-08:00 Pacific Time (US & Canada); Tijuana */
899     {14, QLatin1String("America/Anchorage")},         /* GMT-09:00 Alaska */
900     {15, QLatin1String("Pacific/Honolulu")},          /* GMT-10:00 Hawaii */
901     {16, QLatin1String("Pacific/Apia")},              /* GMT-11:00 Midway Island, Samoa */
902     {17, QLatin1String("Pacific/Auckland")},          /* GMT+12:00 Auckland, Wellington */
903     {18, QLatin1String("Australia/Brisbane")},        /* GMT+10:00 Brisbane, East Australia */
904     {19, QLatin1String("Australia/Adelaide")},        /* GMT+09:30 Adelaide, Central Australia */
905     {20, QLatin1String("Asia/Tokyo")},                /* GMT+09:00 Osaka, Sapporo, Tokyo */
906     {21, QLatin1String("Asia/Singapore")},            /* GMT+08:00 Kuala Lumpur, Singapore */
907     {22, QLatin1String("Asia/Bangkok")},              /* GMT+07:00 Bangkok, Hanoi, Jakarta */
908     {23, QLatin1String("Asia/Calcutta")},             /* GMT+05:30 Kolkata, Chennai, Mumbai, New Delhi, India Standard Time */
909     {24, QLatin1String("Asia/Dubai")},                /* GMT+04:00 Abu Dhabi, Muscat */
910     {25, QLatin1String("Asia/Tehran")},               /* GMT+03:30 Tehran */
911     {26, QLatin1String("Asia/Baghdad")},              /* GMT+03:00 Baghdad */
912     {27, QLatin1String("Asia/Jerusalem")},            /* GMT+02:00 Israel, Jerusalem Standard Time */
913     {28, QLatin1String("America/St_Johns")},          /* GMT-03:30 Newfoundland */
914     {29, QLatin1String("Atlantic/Portugal")},         /* GMT-01:00 Azores */
915     {30, QLatin1String("America/Noronha")},           /* GMT-02:00 Mid-Atlantic */
916     {31, QLatin1String("Africa/Monrovia")},           /* GMT Casablanca, Monrovia */
917     {32, QLatin1String("America/Argentina/Buenos_Aires")}, /* GMT-03:00 Buenos Aires, Georgetown */
918     {33, QLatin1String("America/La_Paz")},            /* GMT-04:00 Caracas, La Paz */
919     {34, QLatin1String("America/New_York")},          /* GMT-05:00 Indiana (East) */
920     {35, QLatin1String("America/Bogota")},            /* GMT-05:00 Bogota, Lima, Quito */
921     {36, QLatin1String("America/Winnipeg")},          /* GMT-06:00 Saskatchewan */
922     {37, QLatin1String("America/Mexico_City")},       /* GMT-06:00 Mexico City, Tegucigalpa */
923     {38, QLatin1String("America/Phoenix")},           /* GMT-07:00 Arizona */
924     {39, QLatin1String("Pacific/Kwajalein")},         /* GMT-12:00 Eniwetok, Kwajalein, Dateline Time */
925     {40, QLatin1String("Pacific/Fiji")},              /* GMT+12:00 Fušál, Kamchatka, Mashall Is. */
926     {41, QLatin1String("Pacific/Noumea")},            /* GMT+11:00 Magadan, Solomon Is., New Caledonia */
927     {42, QLatin1String("Australia/Hobart")},          /* GMT+10:00 Hobart, Tasmania */
928     {43, QLatin1String("Pacific/Guam")},              /* GMT+10:00 Guam, Port Moresby */
929     {44, QLatin1String("Australia/Darwin")},          /* GMT+09:30 Darwin */
930     {45, QLatin1String("Asia/Shanghai")},             /* GMT+08:00 Beijing, Chongqing, Hong Kong SAR, Urumqi */
931     {46, QLatin1String("Asia/Omsk")},                 /* GMT+06:00 Almaty, Novosibirsk, North Central Asia */
932     {47, QLatin1String("Asia/Karachi")},              /* GMT+05:00 Islamabad, Karachi, Tashkent */
933     {48, QLatin1String("Asia/Kabul")},                /* GMT+04:30 Kabul */
934     {49, QLatin1String("Africa/Cairo")},              /* GMT+02:00 Cairo */
935     {50, QLatin1String("Africa/Harare")},             /* GMT+02:00 Harare, Pretoria */
936     {51, QLatin1String("Europe/Moscow")},             /* GMT+03:00 Moscow, St. Petersburg, Volgograd */
937     {53, QLatin1String("Atlantic/Cape_Verde")},       /* GMT-01:00 Cape Verde Is. */
938     {54, QLatin1String("Asia/Tbilisi")},              /* GMT+04:00 Baku, Tbilisi, Yerevan */
939     {55, QLatin1String("America/Tegucigalpa")},       /* GMT-06:00 Central America */
940     {56, QLatin1String("Africa/Nairobi")},            /* GMT+03:00 East Africa, Nairobi */
941     {58, QLatin1String("Asia/Yekaterinburg")},        /* GMT+05:00 Ekaterinburg */
942     {59, QLatin1String("Europe/Helsinki")},           /* GMT+02:00 Helsinki, Riga, Tallinn */
943     {60, QLatin1String("America/Greenland")},         /* GMT-03:00 Greenland */
944     {61, QLatin1String("Asia/Rangoon")},              /* GMT+06:30 Yangon (Rangoon) */
945     {62, QLatin1String("Asia/Katmandu")},             /* GMT+05:45 Kathmandu, Nepal */
946     {63, QLatin1String("Asia/Irkutsk")},              /* GMT+08:00 Irkutsk, Ulaan Bataar */
947     {64, QLatin1String("Asia/Krasnoyarsk")},          /* GMT+07:00 Krasnoyarsk */
948     {65, QLatin1String("America/Santiago")},          /* GMT-04:00 Santiago */
949     {66, QLatin1String("Asia/Colombo")},              /* GMT+06:00 Sri Jayawardenepura, Sri Lanka */
950     {67, QLatin1String("Pacific/Tongatapu")},         /* GMT+13:00 Nuku'alofa, Tonga */
951     {68, QLatin1String("Asia/Vladivostok")},          /* GMT+10:00 Vladivostok */
952     {69, QLatin1String("Africa/Bangui")},             /* GMT+01:00 West Central Africa */
953     {70, QLatin1String("Asia/Yakutsk")},              /* GMT+09:00 Yakutsk */
954     {71, QLatin1String("Asia/Dhaka")},                /* GMT+06:00 Astana, Dhaka */
955     {72, QLatin1String("Asia/Seoul")},                /* GMT+09:00 Seoul, Korea Standard time */
956     {73, QLatin1String("Australia/Perth")},           /* GMT+08:00 Perth, Western Australia */
957     {74, QLatin1String("Asia/Kuwait")},               /* GMT+03:00 Arab, Kuwait, Riyadh */
958     {75, QLatin1String("Asia/Taipei")},               /* GMT+08:00 Taipei */
959     {76, QLatin1String("Australia/Sydney")}          /* GMT+10:00 Canberra, Melbourne, Sydney */
960 };
961 
962 /* Based on "Microsoft Time Zone Index Values"
963  * https://support.microsoft.com/en-gb/help/973627/microsoft-time-zone-index-values
964  *
965  * The mapping is not exact, since the TZID usually refers to a
966  * region of multiple countries, so I always picked one of the countries
967  * in the specified region and used it's TZID.
968  *
969  * The Olson timezones are taken from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
970  *
971  * Note: using std::map, because it allows heterogeneous lookup, i.e. I can lookup the QLatin1String
972  * keys by using QString value, which is not possible with Qt containers.
973  */
974 static const std::map<QLatin1String, QLatin1String, std::less<>> MSSTTZTable = {
975     {QLatin1String("Dateline Standard Time"), QLatin1String("Pacific/Kwajalein")},    /* (GMT-12:00) International Date Line West */
976     {QLatin1String("Samoa Standard Time"), QLatin1String("Pacific/Apia")},            /* (GMT-11:00) Midway Island, Samoa */
977     {QLatin1String("Hawaiian Standard Time"), QLatin1String("Pacific/Honolulu")},     /* (GMT-10:00) Hawaii */
978     {QLatin1String("Alaskan Standard Time"), QLatin1String("America/Anchorage")},     /* (GMT-09:00) Alaska */
979     {QLatin1String("Pacific Standard Time"), QLatin1String("America/Los_Angeles")},   /* (GMT-08:00) Pacific Time (US and Canada); Tijuana */
980     {QLatin1String("Mountain Standard Time"), QLatin1String("America/Denver")},       /* (GMT-07:00) Mountain Time (US and Canada) */
981     {QLatin1String("Mexico Standard Time 2"), QLatin1String("America/Chihuahua")},    /* (GMT-07:00) Chihuahua, La Paz, Mazatlan */
982     {QLatin1String("U.S. Mountain Standard Time"), QLatin1String("America/Phoenix")}, /* (GMT-07:00) Arizona */
983     {QLatin1String("Central Standard Time"), QLatin1String("America/Chicago")},       /* (GMT-06:00) Central Time (US and Canada */
984     {QLatin1String("Canada Central Standard Time"), QLatin1String("America/Winnipeg")},       /* (GMT-06:00) Saskatchewan */
985     {QLatin1String("Mexico Standard Time"), QLatin1String("America/Mexico_City")},    /* (GMT-06:00) Guadalajara, Mexico City, Monterrey */
986     {QLatin1String("Central America Standard Time"), QLatin1String("America/Chicago")},       /* (GMT-06:00) Central America */
987     {QLatin1String("Eastern Standard Time"), QLatin1String("America/New_York")},      /* (GMT-05:00) Eastern Time (US and Canada) */
988     {QLatin1String("U.S. Eastern Standard Time"), QLatin1String("America/New_York")}, /* (GMT-05:00) Indiana (East) */
989     {QLatin1String("S.A. Pacific Standard Time"), QLatin1String("America/Bogota")},   /* (GMT-05:00) Bogota, Lima, Quito */
990     {QLatin1String("Atlantic Standard Time"), QLatin1String("America/Halifax")},      /* (GMT-04:00) Atlantic Time (Canada) */
991     {QLatin1String("S.A. Western Standard Time"), QLatin1String("America/La_Paz")},   /* (GMT-04:00) Caracas, La Paz */
992     {QLatin1String("Pacific S.A. Standard Time"), QLatin1String("America/Santiago")}, /* (GMT-04:00) Santiago */
993     {QLatin1String("Newfoundland and Labrador Standard Time"), QLatin1String("America/St_Johns")},    /* (GMT-03:30) Newfoundland and Labrador */
994     {QLatin1String("E. South America Standard Time"), QLatin1String("America/Brazil")},       /* (GMT-03:00) Brasilia */
995     {QLatin1String("S.A. Eastern Standard Time"), QLatin1String("America/Argentina/Buenos_Aires")},   /* (GMT-03:00) Buenos Aires, Georgetown */
996     {QLatin1String("Greenland Standard Time"), QLatin1String("America/Greenland")},   /* (GMT-03:00) Greenland */
997     {QLatin1String("Mid-Atlantic Standard Time"), QLatin1String("America/Noronha")},  /* (GMT-02:00) Mid-Atlantic */
998     {QLatin1String("Azores Standard Time"), QLatin1String("Atlantic/Portugal")},      /* (GMT-01:00) Azores */
999     {QLatin1String("Cape Verde Standard Time"), QLatin1String("Atlantic/Cape_Verde")},        /* (GMT-01:00) Cape Verde Islands */
1000     {QLatin1String("GMT Standard Time"), QLatin1String("Europe/London")},             /* (GMT) Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London */
1001     {QLatin1String("Greenwich Standard Time"), QLatin1String("Africa/Casablanca")},   /* (GMT) Casablanca, Monrovia */
1002     {QLatin1String("Central Europe Standard Time"), QLatin1String("Europe/Prague")},  /* (GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague */
1003     {QLatin1String("Central European Standard Time"), QLatin1String("Europe/Sarajevo")},      /* (GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb */
1004     {QLatin1String("Romance Standard Time"), QLatin1String("Europe/Brussels")},       /* (GMT+01:00) Brussels, Copenhagen, Madrid, Paris */
1005     {QLatin1String("W. Europe Standard Time"), QLatin1String("Europe/Amsterdam")},    /* (GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna */
1006     {QLatin1String("W. Central Africa Standard Time"), QLatin1String("Africa/Bangui")},       /* (GMT+01:00) West Central Africa */
1007     {QLatin1String("E. Europe Standard Time"), QLatin1String("Europe/Bucharest")},    /* (GMT+02:00) Bucharest */
1008     {QLatin1String("Egypt Standard Time"), QLatin1String("Africa/Cairo")},            /* (GMT+02:00) Cairo */
1009     {QLatin1String("FLE Standard Time"), QLatin1String("Europe/Helsinki")},           /* (GMT+02:00) Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius */
1010     {QLatin1String("GTB Standard Time"), QLatin1String("Europe/Athens")},             /* (GMT+02:00) Athens, Istanbul, Minsk */
1011     {QLatin1String("Israel Standard Time"), QLatin1String("Europe/Athens")},          /* (GMT+02:00) Jerusalem */
1012     {QLatin1String("South Africa Standard Time"), QLatin1String("Africa/Harare")},    /* (GMT+02:00) Harare, Pretoria */
1013     {QLatin1String("Russian Standard Time"), QLatin1String("Europe/Moscow")},         /* (GMT+03:00) Moscow, St. Petersburg, Volgograd */
1014     {QLatin1String("Arab Standard Time"), QLatin1String("Asia/Kuwait")},              /* (GMT+03:00) Kuwait, Riyadh */
1015     {QLatin1String("E. Africa Standard Time"), QLatin1String("Africa/Nairobi")},      /* (GMT+03:00) Nairobi */
1016     {QLatin1String("Arabic Standard Time"), QLatin1String("Asia/Baghdad")},           /* (GMT+03:00) Baghdad */
1017     {QLatin1String("Iran Standard Time"), QLatin1String("Asia/Tehran")},              /* (GMT+03:30) Tehran */
1018     {QLatin1String("Arabian Standard Time"), QLatin1String("Asia/Dubai")},            /* (GMT+04:00) Abu Dhabi, Muscat */
1019     {QLatin1String("Caucasus Standard Time"), QLatin1String("Asia/Tbilisi")},         /* (GMT+04:00) Baku, Tbilisi, Yerevan */
1020     {QLatin1String("Transitional Islamic State of Afghanistan Standard Time"), QLatin1String("Asia/Kabul")},  /* (GMT+04:30) Kabul */
1021     {QLatin1String("Ekaterinburg Standard Time"), QLatin1String("Asia/Yekaterinburg")},       /* (GMT+05:00) Ekaterinburg */
1022     {QLatin1String("West Asia Standard Time"), QLatin1String("Asia/Karachi")},        /* (GMT+05:00) Islamabad, Karachi, Tashkent */
1023     {QLatin1String("India Standard Time"), QLatin1String("Asia/Calcutta")},           /* (GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi */
1024     {QLatin1String("Nepal Standard Time"), QLatin1String("Asia/Calcutta")},           /* (GMT+05:45) Kathmandu */
1025     {QLatin1String("Central Asia Standard Time"), QLatin1String("Asia/Dhaka")},       /* (GMT+06:00) Astana, Dhaka */
1026     {QLatin1String("Sri Lanka Standard Time"), QLatin1String("Asia/Colombo")},        /* (GMT+06:00) Sri Jayawardenepura */
1027     {QLatin1String("N. Central Asia Standard Time"), QLatin1String("Asia/Omsk")},     /* (GMT+06:00) Almaty, Novosibirsk */
1028     {QLatin1String("Myanmar Standard Time"), QLatin1String("Asia/Rangoon")},          /* (GMT+06:30) Yangon Rangoon */
1029     {QLatin1String("S.E. Asia Standard Time"), QLatin1String("Asia/Bangkok")},        /* (GMT+07:00) Bangkok, Hanoi, Jakarta */
1030     {QLatin1String("North Asia Standard Time"), QLatin1String("Asia/Krasnoyarsk")},   /* (GMT+07:00) Krasnoyarsk */
1031     {QLatin1String("China Standard Time"), QLatin1String("Asia/Shanghai")},           /* (GMT+08:00) Beijing, Chongqing, Hong Kong SAR, Urumqi */
1032     {QLatin1String("Singapore Standard Time"), QLatin1String("Asia/Singapore")},      /* (GMT+08:00) Kuala Lumpur, Singapore */
1033     {QLatin1String("Taipei Standard Time"), QLatin1String("Asia/Taipei")},            /* (GMT+08:00) Taipei */
1034     {QLatin1String("W. Australia Standard Time"), QLatin1String("Australia/Perth")},  /* (GMT+08:00) Perth */
1035     {QLatin1String("North Asia East Standard Time"), QLatin1String("Asia/Irkutsk")},  /* (GMT+08:00) Irkutsk, Ulaanbaatar */
1036     {QLatin1String("Korea Standard Time"), QLatin1String("Asia/Seoul")},              /* (GMT+09:00) Seoul */
1037     {QLatin1String("Tokyo Standard Time"), QLatin1String("Asia/Tokyo")},              /* (GMT+09:00) Osaka, Sapporo, Tokyo */
1038     {QLatin1String("Yakutsk Standard Time"), QLatin1String("Asia/Yakutsk")},          /* (GMT+09:00) Yakutsk */
1039     {QLatin1String("A.U.S. Central Standard Time"), QLatin1String("Australia/Darwin")},       /* (GMT+09:30) Darwin */
1040     {QLatin1String("Cen. Australia Standard Time"), QLatin1String("Australia/Adelaide")},     /* (GMT+09:30) Adelaide */
1041     {QLatin1String("A.U.S. Eastern Standard Time"), QLatin1String("Australia/Sydney")},       /* (GMT+10:00) Canberra, Melbourne, Sydney */
1042     {QLatin1String("E. Australia Standard Time"), QLatin1String("Australia/Brisbane")},       /* (GMT+10:00) Brisbane */
1043     {QLatin1String("Tasmania Standard Time"), QLatin1String("Australia/Hobart")},     /* (GMT+10:00) Hobart */
1044     {QLatin1String("Vladivostok Standard Time"), QLatin1String("Asia/Vladivostok")},  /* (GMT+10:00) Vladivostok */
1045     {QLatin1String("West Pacific Standard Time"), QLatin1String("Pacific/Guam")},     /* (GMT+10:00) Guam, Port Moresby */
1046     {QLatin1String("Central Pacific Standard Time"), QLatin1String("Pacific/Noumea")},        /* (GMT+11:00) Magadan, Solomon Islands, New Caledonia */
1047     {QLatin1String("Fiji Islands Standard Time"), QLatin1String("Pacific/Fiji")},     /* (GMT+12:00) Fiji Islands, Kamchatka, Marshall Islands */
1048     {QLatin1String("New Zealand Standard Time"), QLatin1String("Pacific/Auckland")},  /* (GMT+12:00) Auckland, Wellington */
1049     {QLatin1String("Tonga Standard Time"), QLatin1String("Pacific/Tongatapu")},       /* (GMT+13:00) Nuku'alofa */
1050     {QLatin1String("Azerbaijan Standard Time"), QLatin1String("America/Argentina/Buenos_Aires")},     /* (GMT-03:00) Buenos Aires */
1051     {QLatin1String("Middle East Standard Time"), QLatin1String("Asia/Beirut")},       /* (GMT+02:00) Beirut */
1052     {QLatin1String("Jordan Standard Time"), QLatin1String("Asia/Amman")},             /* (GMT+02:00) Amman */
1053     {QLatin1String("Central Standard Time (Mexico)"), QLatin1String("America/Mexico_City")},  /* (GMT-06:00) Guadalajara, Mexico City, Monterrey - New */
1054     {QLatin1String("Mountain Standard Time (Mexico)"), QLatin1String("America/Ojinaga")},     /* (GMT-07:00) Chihuahua, La Paz, Mazatlan - New */
1055     {QLatin1String("Pacific Standard Time (Mexico)"), QLatin1String("America/Tijuana")},      /* (GMT-08:00) Tijuana, Baja California */
1056     {QLatin1String("Namibia Standard Time"), QLatin1String("Africa/Windhoek")},       /* (GMT+02:00) Windhoek */
1057     {QLatin1String("Georgian Standard Time"), QLatin1String("Asia/Tbilisi")},         /* (GMT+03:00) Tbilisi */
1058     {QLatin1String("Central Brazilian Standard Time"), QLatin1String("America/Manaus")},      /*(GMT-04:00) Manaus */
1059     {QLatin1String("Montevideo Standard Time"), QLatin1String("America/Montevideo")},        /* (GMT-03:00) Montevideo */
1060     {QLatin1String("Armenian Standard Time"), QLatin1String("Asia/Yerevan")},         /* (GMT+04:00) Yerevan */
1061     {QLatin1String("Venezuela Standard Time"), QLatin1String("America/Caracas")},     /* (GMT-04:30) Caracas */
1062     {QLatin1String("Argentina Standard Time"), QLatin1String("America/Argentina/Buenos_Aires")},      /* (GMT-03:00) Buenos Aires */
1063     {QLatin1String("Morocco Standard Time"), QLatin1String("Africa/Casablanca")},     /* (GMT) Casablanca */
1064     {QLatin1String("Pakistan Standard Time"), QLatin1String("Asia/Karachi")},         /* (GMT+05:00) Islamabad, Karachi */
1065     {QLatin1String("Mauritius Standard Time"), QLatin1String("Indian/Mauritius")},    /* (GMT+04:00) Port Louis */
1066     {QLatin1String("UTC"), QLatin1String("UTC")},                                     /* (GMT) Coordinated Universal Time */
1067     {QLatin1String("Paraguay Standard Time"), QLatin1String("America/Asuncion")},     /* (GMT-04:00) Asuncion */
1068     {QLatin1String("Kamchatka Standard Time"), QLatin1String("Asia/Kamchatka")},      /* (GMT+12:00) Petropavlovsk-Kamchatsky */
1069 };
1070 } // namespace
1071 
checkAndConverCDOTZID(const QString & tzid,const EventPtr & event)1072 QString Private::checkAndConverCDOTZID(const QString &tzid, const EventPtr& event)
1073 {
1074     /* Try to match the @tzid to any valid timezone we know. */
1075     QTimeZone tz(tzid.toLatin1());
1076     if (tz.isValid()) {
1077         /* Yay, @tzid is a valid TZID in Olson format */
1078         return tzid;
1079     }
1080 
1081     /* Damn, no match. Parse the iCal and try to find X-MICROSOFT-CDO-TZID
1082      * property that we can match against the MSCDOTZIDTable */
1083     KCalendarCore::ICalFormat format;
1084     /* Use a copy of @event, otherwise it would be deleted when ptr is destroyed */
1085     KCalendarCore::Incidence::Ptr incidence = event.dynamicCast<KCalendarCore::Incidence>();
1086     const QString vcard = format.toICalString(incidence);
1087     const QStringList properties = vcard.split(QLatin1Char('\n'));
1088     int CDOId = -1;
1089     for (const QString &property : properties) {
1090         if (property.startsWith(u"X-MICROSOFT-CDO-TZID")) {
1091             QStringList parsed = property.split(QLatin1Char(':'));
1092             if (parsed.length() != 2) {
1093                 break;
1094             }
1095 
1096             CDOId = parsed.at(1).toInt();
1097             break;
1098         }
1099     }
1100 
1101     /* Wheeee, we have X-MICROSOFT-CDO-TZID, try to map it to Olson format */
1102     if (CDOId > -1) {
1103 
1104         /* *sigh* Some expert in MS assigned the same ID to two different timezones... */
1105         if (CDOId == 2) {
1106 
1107             /* GMT Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London */
1108             if (tzid.contains(QLatin1String("Dublin")) ||
1109                     tzid.contains(QLatin1String("Edinburgh")) ||
1110                     tzid.contains(QLatin1String("Lisbon")) ||
1111                     tzid.contains(QLatin1String("London")))
1112             {
1113                 return QStringLiteral("Europe/London");
1114             }
1115 
1116             /* GMT+01:00 Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb */
1117             else if (tzid.contains(QLatin1String("Sarajevo")) ||
1118                      tzid.contains(QLatin1String("Skopje")) ||
1119                      tzid.contains(QLatin1String("Sofija")) ||
1120                      tzid.contains(QLatin1String("Vilnius")) ||
1121                      tzid.contains(QLatin1String("Warsaw")) ||
1122                      tzid.contains(QLatin1String("Zagreb")))
1123             {
1124                 return QStringLiteral("Europe/Sarajevo");
1125             }
1126         }
1127 
1128         const auto it = MSCDOTZIDTable.find(CDOId);
1129         if (it != MSCDOTZIDTable.cend()) {
1130             return it->second;
1131         }
1132     }
1133 
1134     /* We failed to map to X-MICROSOFT-CDO-TZID. Let's try mapping the TZID
1135      * onto the Microsoft Standard Time Zones */
1136     const auto it = MSSTTZTable.find(tzid);
1137     if (it != MSSTTZTable.cend()) {
1138         return it->second;
1139     }
1140 
1141     /* Fail/ Just return the original TZID and hope Google will accept it
1142      * (though we know it won't) */
1143     return tzid;
1144 }
1145 
1146 } // namespace CalendarService
1147 
1148 } // namespace KGAPI2
1149