File indexing completed on 2024-05-12 05:22:13

0001 /*
0002  * This file is part of LibKGAPI library
0003  *
0004  * SPDX-FileCopyrightText: 2013 Daniel Vrátil <dvratil@redhat.com>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0007  */
0008 
0009 #include "calendarservice.h"
0010 #include "calendar.h"
0011 #include "debug.h"
0012 #include "event.h"
0013 #include "reminder.h"
0014 #include "utils.h"
0015 
0016 #include <KCalendarCore/Alarm>
0017 #include <KCalendarCore/Attendee>
0018 #include <KCalendarCore/Event>
0019 #include <KCalendarCore/ICalFormat>
0020 #include <KCalendarCore/Person>
0021 #include <KCalendarCore/Recurrence>
0022 #include <KCalendarCore/RecurrenceRule>
0023 
0024 #include <QJsonDocument>
0025 #include <QNetworkRequest>
0026 #include <QTimeZone>
0027 #include <QUrlQuery>
0028 #include <QVariant>
0029 
0030 #include <map>
0031 #include <memory>
0032 
0033 namespace KGAPI2
0034 {
0035 
0036 namespace CalendarService
0037 {
0038 
0039 namespace Private
0040 {
0041 KCalendarCore::DateList parseRDate(const QString &rule);
0042 
0043 ObjectPtr JSONToCalendar(const QVariantMap &data);
0044 ObjectPtr JSONToEvent(const QVariantMap &data, const QString &timezone = QString());
0045 
0046 /**
0047  * Checks whether TZID is in Olson format and converts it to it if necessary
0048  *
0049  * This is mainly to handle crazy Microsoft TZIDs like
0050  * "(GMT) Greenwich Mean Time/Dublin/Edinburgh/London", because Google only
0051  * accepts TZIDs in Olson format ("Europe/London").
0052  *
0053  * It first tries to match the given \p tzid to all TZIDs in KTimeZones::zones().
0054  * If it fails, it parses the \p event, looking for X-MICROSOFT-CDO-TZID
0055  * property and than matches it to Olson-formatted TZID using a table.
0056  *
0057  * When the method fails to process the TZID, it returns the original \p tzid
0058  * in hope, that Google will cope with it.
0059  */
0060 QString checkAndConverCDOTZID(const QString &tzid, const EventPtr &event);
0061 
0062 static const QUrl GoogleApisUrl(QStringLiteral("https://www.googleapis.com"));
0063 static const QString CalendarListBasePath(QStringLiteral("/calendar/v3/users/me/calendarList"));
0064 static const QString CalendarBasePath(QStringLiteral("/calendar/v3/calendars"));
0065 }
0066 
0067 QNetworkRequest prepareRequest(const QUrl &url)
0068 {
0069     QNetworkRequest request(url);
0070     request.setRawHeader("GData-Version", CalendarService::APIVersion().toLatin1());
0071 
0072     return request;
0073 }
0074 
0075 /************* URLS **************/
0076 
0077 QUrl fetchCalendarsUrl()
0078 {
0079     QUrl url(Private::GoogleApisUrl);
0080     url.setPath(Private::CalendarListBasePath);
0081     return url;
0082 }
0083 
0084 QUrl fetchCalendarUrl(const QString &calendarID)
0085 {
0086     QUrl url(Private::GoogleApisUrl);
0087     url.setPath(Private::CalendarListBasePath % QLatin1Char('/') % calendarID);
0088     return url;
0089 }
0090 
0091 QUrl updateCalendarUrl(const QString &calendarID)
0092 {
0093     QUrl url(Private::GoogleApisUrl);
0094     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID);
0095     return url;
0096 }
0097 
0098 QUrl createCalendarUrl()
0099 {
0100     QUrl url(Private::GoogleApisUrl);
0101     url.setPath(Private::CalendarBasePath);
0102     return url;
0103 }
0104 
0105 QUrl removeCalendarUrl(const QString &calendarID)
0106 {
0107     QUrl url(Private::GoogleApisUrl);
0108     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID);
0109     return url;
0110 }
0111 
0112 QUrl fetchEventsUrl(const QString &calendarID)
0113 {
0114     QUrl url(Private::GoogleApisUrl);
0115     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events"));
0116     return url;
0117 }
0118 
0119 QUrl fetchEventUrl(const QString &calendarID, const QString &eventID)
0120 {
0121     QUrl url(Private::GoogleApisUrl);
0122     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events/") % eventID);
0123     return url;
0124 }
0125 
0126 namespace
0127 {
0128 
0129 QString sendUpdatesPolicyToString(SendUpdatesPolicy policy)
0130 {
0131     switch (policy) {
0132     case SendUpdatesPolicy::All:
0133         return QStringLiteral("all");
0134     case SendUpdatesPolicy::ExternalOnly:
0135         return QStringLiteral("externalOnly");
0136     case SendUpdatesPolicy::None:
0137         return QStringLiteral("none");
0138     }
0139     Q_UNREACHABLE();
0140 }
0141 
0142 static const QString sendUpatesQueryParam = QStringLiteral("sendUpdates");
0143 static const QString destinationQueryParam = QStringLiteral("destination");
0144 }
0145 
0146 QUrl updateEventUrl(const QString &calendarID, const QString &eventID, SendUpdatesPolicy updatePolicy)
0147 {
0148     QUrl url(Private::GoogleApisUrl);
0149     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events/") % eventID);
0150     QUrlQuery query(url);
0151     query.addQueryItem(sendUpatesQueryParam, sendUpdatesPolicyToString(updatePolicy));
0152     url.setQuery(query);
0153     return url;
0154 }
0155 
0156 QUrl createEventUrl(const QString &calendarID, SendUpdatesPolicy updatePolicy)
0157 {
0158     QUrl url(Private::GoogleApisUrl);
0159     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events"));
0160     QUrlQuery query(url);
0161     query.addQueryItem(sendUpatesQueryParam, sendUpdatesPolicyToString(updatePolicy));
0162     url.setQuery(query);
0163     return url;
0164 }
0165 
0166 QUrl importEventUrl(const QString &calendarID, SendUpdatesPolicy updatePolicy)
0167 {
0168     QUrl url(Private::GoogleApisUrl);
0169     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events") % QLatin1StringView("/import"));
0170     QUrlQuery query(url);
0171     query.addQueryItem(sendUpatesQueryParam, sendUpdatesPolicyToString(updatePolicy));
0172     url.setQuery(query);
0173     return url;
0174 }
0175 
0176 QUrl removeEventUrl(const QString &calendarID, const QString &eventID)
0177 {
0178     QUrl url(Private::GoogleApisUrl);
0179     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % calendarID % QLatin1StringView("/events/") % eventID);
0180     return url;
0181 }
0182 
0183 QUrl moveEventUrl(const QString &sourceCalendar, const QString &destCalendar, const QString &eventID)
0184 {
0185     QUrl url(Private::GoogleApisUrl);
0186     url.setPath(Private::CalendarBasePath % QLatin1Char('/') % sourceCalendar % QLatin1StringView("/events/") % eventID % QLatin1StringView("/move"));
0187     QUrlQuery query(url);
0188     query.addQueryItem(destinationQueryParam, destCalendar);
0189     url.setQuery(query);
0190     return url;
0191 }
0192 
0193 QUrl freeBusyQueryUrl()
0194 {
0195     QUrl url(Private::GoogleApisUrl);
0196     url.setPath(QStringLiteral("/calendar/v3/freeBusy"));
0197     return url;
0198 }
0199 
0200 namespace
0201 {
0202 
0203 static const auto kindParam = QStringLiteral("kind");
0204 static const auto idParam = QStringLiteral("id");
0205 static const auto etagParam = QStringLiteral("etag");
0206 
0207 static const auto nextSyncTokenParam = QStringLiteral("nextSyncToken");
0208 static const auto nextPageTokenParam = QStringLiteral("nextPageToken");
0209 static const auto pageTokenParam = QStringLiteral("pageToken");
0210 static const auto itemsParam = QStringLiteral("items");
0211 
0212 static const auto calendarSummaryParam = QStringLiteral("summary");
0213 static const auto calendarDescriptionParam = QStringLiteral("description");
0214 static const auto calendarLocationParam = QStringLiteral("location");
0215 static const auto calendarTimezoneParam = QStringLiteral("timeZone");
0216 static const auto calendarBackgroundColorParam = QStringLiteral("backgroundColor");
0217 static const auto calendarForegroundColorParam = QStringLiteral("foregroundColor");
0218 static const auto calendarAccessRoleParam = QStringLiteral("accessRole");
0219 static const auto calendarDefaultRemindersParam = QStringLiteral("defaultReminders");
0220 static const auto reminderMethodParam = QStringLiteral("method");
0221 static const auto reminderMinutesParam = QStringLiteral("minutes");
0222 
0223 static const auto eventiCalUIDParam = QStringLiteral("iCalUID");
0224 static const auto eventStatusParam = QStringLiteral("status");
0225 static const auto eventCreatedParam = QStringLiteral("created");
0226 static const auto eventUpdatedParam = QStringLiteral("updated");
0227 static const auto eventSummaryParam = QStringLiteral("summary");
0228 static const auto eventDescriptionParam = QStringLiteral("description");
0229 static const auto eventLocationParam = QStringLiteral("location");
0230 static const auto eventStartPram = QStringLiteral("start");
0231 static const auto eventEndParam = QStringLiteral("end");
0232 static const auto eventOriginalStartTimeParam = QStringLiteral("originalStartTime");
0233 static const auto eventTransparencyParam = QStringLiteral("transparency");
0234 static const auto eventOrganizerParam = QStringLiteral("organizer");
0235 static const auto eventAttendeesParam = QStringLiteral("attendees");
0236 static const auto eventRecurrenceParam = QStringLiteral("recurrence");
0237 static const auto eventRemindersParam = QStringLiteral("reminders");
0238 static const auto eventExtendedPropertiesParam = QStringLiteral("extendedProperties");
0239 static const auto eventRecurringEventIdParam = QStringLiteral("recurringEventId");
0240 
0241 static const auto attendeeDisplayNameParam = QStringLiteral("displayName");
0242 static const auto attendeeEmailParam = QStringLiteral("email");
0243 static const auto attendeeResponseStatusParam = QStringLiteral("responseStatus");
0244 static const auto attendeeOptionalParam = QStringLiteral("optional");
0245 
0246 static const auto organizerDisplayNameParam = QStringLiteral("displayName");
0247 static const auto organizerEmailParam = QStringLiteral("email");
0248 
0249 static const auto reminderUseDefaultParam = QStringLiteral("useDefault");
0250 static const auto reminderOverridesParam = QStringLiteral("overrides");
0251 
0252 static const auto propertyPrivateParam = QStringLiteral("private");
0253 static const auto propertySharedParam = QStringLiteral("shared");
0254 
0255 static const auto dateParam = QStringLiteral("date");
0256 static const auto dateTimeParam = QStringLiteral("dateTime");
0257 static const auto timeZoneParam = QStringLiteral("timeZone");
0258 
0259 static const auto calendarListKind = QLatin1StringView("calendar#calendarList");
0260 static const auto calendarListEntryKind = QLatin1StringView("calendar#calendarListEntry");
0261 static const auto calendarKind = QLatin1StringView("calendar#calendar");
0262 static const auto eventKind = QLatin1StringView("calendar#event");
0263 static const auto eventsKind = QLatin1StringView("calendar#events");
0264 
0265 static const auto writerAccessRole = QLatin1StringView("writer");
0266 static const auto ownerAccessRole = QLatin1StringView("owner");
0267 static const auto emailMethod = QLatin1StringView("email");
0268 static const auto popupMethod = QLatin1StringView("popup");
0269 
0270 static const auto confirmedStatus = QLatin1StringView("confirmed");
0271 static const auto canceledStatus = QLatin1StringView("cancelled");
0272 static const auto tentativeStatus = QLatin1StringView("tentative");
0273 static const auto acceptedStatus = QLatin1StringView("accepted");
0274 static const auto needsActionStatus = QLatin1StringView("needsAction");
0275 static const auto transparentTransparency = QLatin1StringView("transparent");
0276 static const auto opaqueTransparency = QLatin1StringView("opaque");
0277 static const auto declinedStatus = QLatin1StringView("declined");
0278 static const auto categoriesProperty = QLatin1StringView("categories");
0279 
0280 static const auto hangoutLinkParam = QStringLiteral("hangoutLink");
0281 
0282 }
0283 
0284 QString APIVersion()
0285 {
0286     return QStringLiteral("3");
0287 }
0288 
0289 CalendarPtr JSONToCalendar(const QByteArray &jsonData)
0290 {
0291     const auto document = QJsonDocument::fromJson(jsonData);
0292     const auto calendar = document.toVariant().toMap();
0293 
0294     if (calendar.value(kindParam).toString() != calendarListEntryKind && calendar.value(kindParam).toString() != calendarKind) {
0295         return CalendarPtr();
0296     }
0297 
0298     return Private::JSONToCalendar(calendar).staticCast<Calendar>();
0299 }
0300 
0301 ObjectPtr Private::JSONToCalendar(const QVariantMap &data)
0302 {
0303     auto calendar = CalendarPtr::create();
0304 
0305     const auto id = QUrl::fromPercentEncoding(data.value(idParam).toByteArray());
0306     calendar->setUid(id);
0307     calendar->setEtag(data.value(etagParam).toString());
0308     calendar->setTitle(data.value(calendarSummaryParam).toString());
0309     calendar->setDetails(data.value(calendarDescriptionParam).toString());
0310     calendar->setLocation(data.value(calendarLocationParam).toString());
0311     calendar->setTimezone(data.value(calendarTimezoneParam).toString());
0312     calendar->setBackgroundColor(QColor(data.value(calendarBackgroundColorParam).toString()));
0313     calendar->setForegroundColor(QColor(data.value(calendarForegroundColorParam).toString()));
0314 
0315     if ((data.value(calendarAccessRoleParam).toString() == writerAccessRole) || (data.value(calendarAccessRoleParam).toString() == ownerAccessRole)) {
0316         calendar->setEditable(true);
0317     } else {
0318         calendar->setEditable(false);
0319     }
0320 
0321     const auto reminders = data.value(calendarDefaultRemindersParam).toList();
0322     for (const auto &r : reminders) {
0323         const auto reminder = r.toMap();
0324 
0325         auto rem = ReminderPtr::create();
0326         if (reminder.value(reminderMethodParam).toString() == emailMethod) {
0327             rem->setType(KCalendarCore::Alarm::Email);
0328         } else if (reminder.value(reminderMethodParam).toString() == popupMethod) {
0329             rem->setType(KCalendarCore::Alarm::Display);
0330         } else {
0331             rem->setType(KCalendarCore::Alarm::Invalid);
0332         }
0333 
0334         rem->setStartOffset(KCalendarCore::Duration(reminder.value(reminderMinutesParam).toInt() * (-60)));
0335 
0336         calendar->addDefaultReminer(rem);
0337     }
0338 
0339     return calendar.dynamicCast<Object>();
0340 }
0341 
0342 QByteArray calendarToJSON(const CalendarPtr &calendar)
0343 {
0344     QVariantMap entry;
0345 
0346     if (!calendar->uid().isEmpty()) {
0347         entry.insert(idParam, calendar->uid());
0348     }
0349 
0350     entry.insert(calendarSummaryParam, calendar->title());
0351     entry.insert(calendarDescriptionParam, calendar->details());
0352     entry.insert(calendarLocationParam, calendar->location());
0353     if (!calendar->timezone().isEmpty()) {
0354         entry.insert(calendarTimezoneParam, calendar->timezone());
0355     }
0356 
0357     const auto document = QJsonDocument::fromVariant(entry);
0358     return document.toJson(QJsonDocument::Compact);
0359 }
0360 
0361 ObjectsList parseCalendarJSONFeed(const QByteArray &jsonFeed, FeedData &feedData)
0362 {
0363     const auto document = QJsonDocument::fromJson(jsonFeed);
0364     const auto data = document.toVariant().toMap();
0365 
0366     ObjectsList list;
0367 
0368     if (data.value(kindParam).toString() == calendarListKind) {
0369         if (data.contains(nextPageTokenParam)) {
0370             feedData.nextPageUrl = fetchCalendarsUrl();
0371             QUrlQuery query(feedData.nextPageUrl);
0372             query.addQueryItem(pageTokenParam, data.value(nextPageTokenParam).toString());
0373             feedData.nextPageUrl.setQuery(query);
0374         }
0375     } else {
0376         return {};
0377     }
0378 
0379     const auto items = data.value(itemsParam).toList();
0380     list.reserve(items.size());
0381     for (const auto &i : items) {
0382         list.push_back(Private::JSONToCalendar(i.toMap()));
0383     }
0384 
0385     return list;
0386 }
0387 
0388 EventPtr JSONToEvent(const QByteArray &jsonData)
0389 {
0390     QJsonParseError error;
0391     QJsonDocument document = QJsonDocument::fromJson(jsonData, &error);
0392     if (error.error != QJsonParseError::NoError) {
0393         qCWarning(KGAPIDebug) << "Error parsing event JSON: " << error.errorString();
0394     }
0395     QVariantMap data = document.toVariant().toMap();
0396     if (data.value(kindParam).toString() != eventKind) {
0397         return EventPtr();
0398     }
0399 
0400     return Private::JSONToEvent(data).staticCast<Event>();
0401 }
0402 
0403 namespace
0404 {
0405 
0406 struct ParsedDt {
0407     QDateTime dt;
0408     bool isAllDay;
0409 };
0410 
0411 ParsedDt parseDt(const QVariantMap &data, const QString &timezone, bool isDtEnd)
0412 {
0413     if (data.contains(dateParam)) {
0414         auto dt = QDateTime::fromString(data.value(dateParam).toString(), Qt::ISODate);
0415         if (isDtEnd) {
0416             // Google reports all-day events to end on the next day, e.g. a
0417             // Monday all-day event will be reporting as starting on Monday and
0418             // ending on Tuesday, while KCalendarCore/iCal uses the same day for
0419             // dtEnd, so adjust the end date here.
0420             dt = dt.addDays(-1);
0421         }
0422         return {dt, true};
0423     } else if (data.contains(dateTimeParam)) {
0424         auto dt = Utils::rfc3339DateFromString(data.value(dateTimeParam).toString());
0425         // If there's a timezone specified in the "start" entity, then use it
0426         if (data.contains(timeZoneParam)) {
0427             const QTimeZone tz = QTimeZone(data.value(timeZoneParam).toString().toUtf8());
0428             if (tz.isValid()) {
0429                 dt = dt.toTimeZone(tz);
0430             } else {
0431                 qCWarning(KGAPIDebug) << "Invalid timezone" << data.value(timeZoneParam).toString();
0432             }
0433 
0434             // Otherwise try to fallback to calendar-wide timezone
0435         } else if (!timezone.isEmpty()) {
0436             const QTimeZone tz(timezone.toUtf8());
0437             if (tz.isValid()) {
0438                 dt.setTimeZone(tz);
0439             } else {
0440                 qCWarning(KGAPIDebug) << "Invalid timezone" << timezone;
0441             }
0442         }
0443         return {dt, false};
0444     } else {
0445         return {{}, false};
0446     }
0447 }
0448 
0449 void setEventCategories(EventPtr &event, const QVariantMap &properties)
0450 {
0451     for (auto iter = properties.cbegin(), end = properties.cend(); iter != end; ++iter) {
0452         if (iter.key() == categoriesProperty) {
0453             event->setCategories(iter.value().toString());
0454         }
0455     }
0456 }
0457 
0458 } // namespace
0459 
0460 ObjectPtr Private::JSONToEvent(const QVariantMap &data, const QString &timezone)
0461 {
0462     auto event = EventPtr::create();
0463 
0464     event->setId(data.value(idParam).toString());
0465     event->setHangoutLink(data.value(hangoutLinkParam).toString());
0466     event->setUid(data.value(eventiCalUIDParam).toString());
0467     event->setEtag(data.value(etagParam).toString());
0468 
0469     if (data.value(eventStatusParam).toString() == confirmedStatus) {
0470         event->setStatus(KCalendarCore::Incidence::StatusConfirmed);
0471     } else if (data.value(eventStatusParam).toString() == canceledStatus) {
0472         event->setStatus(KCalendarCore::Incidence::StatusCanceled);
0473         event->setDeleted(true);
0474     } else if (data.value(eventStatusParam).toString() == tentativeStatus) {
0475         event->setStatus(KCalendarCore::Incidence::StatusTentative);
0476     } else {
0477         event->setStatus(KCalendarCore::Incidence::StatusNone);
0478     }
0479 
0480     event->setCreated(Utils::rfc3339DateFromString(data.value(eventCreatedParam).toString()));
0481     event->setLastModified(Utils::rfc3339DateFromString(data.value(eventUpdatedParam).toString()));
0482     event->setSummary(data.value(eventSummaryParam).toString());
0483     event->setDescription(data.value(eventDescriptionParam).toString());
0484     event->setLocation(data.value(eventLocationParam).toString());
0485 
0486     const auto dtStart = parseDt(data.value(eventStartPram).toMap(), timezone, false);
0487     event->setDtStart(dtStart.dt);
0488     event->setAllDay(dtStart.isAllDay);
0489 
0490     const auto dtEnd = parseDt(data.value(eventEndParam).toMap(), timezone, true);
0491     event->setDtEnd(dtEnd.dt);
0492 
0493     if (data.contains(eventOriginalStartTimeParam)) {
0494         const auto recurrenceId = parseDt(data.value(eventOriginalStartTimeParam).toMap(), timezone, false);
0495         event->setRecurrenceId(recurrenceId.dt);
0496     }
0497 
0498     if (data.value(eventTransparencyParam).toString() == transparentTransparency) {
0499         event->setTransparency(Event::Transparent);
0500     } else { /* Assume opaque as default transparency */
0501         event->setTransparency(Event::Opaque);
0502     }
0503 
0504     const auto attendees = data.value(eventAttendeesParam).toList();
0505     for (const auto &a : attendees) {
0506         const auto att = a.toMap();
0507         KCalendarCore::Attendee attendee(att.value(attendeeDisplayNameParam).toString(), att.value(attendeeEmailParam).toString());
0508         const auto responseStatus = att.value(attendeeResponseStatusParam).toString();
0509         if (responseStatus == acceptedStatus) {
0510             attendee.setStatus(KCalendarCore::Attendee::Accepted);
0511         } else if (responseStatus == declinedStatus) {
0512             attendee.setStatus(KCalendarCore::Attendee::Declined);
0513         } else if (responseStatus == tentativeStatus) {
0514             attendee.setStatus(KCalendarCore::Attendee::Tentative);
0515         } else {
0516             attendee.setStatus(KCalendarCore::Attendee::NeedsAction);
0517         }
0518 
0519         if (att.value(attendeeOptionalParam).toBool()) {
0520             attendee.setRole(KCalendarCore::Attendee::OptParticipant);
0521         }
0522         const auto uid = att.value(idParam).toString();
0523         if (!uid.isEmpty()) {
0524             attendee.setUid(uid);
0525         } else {
0526             // Set some UID, just so that the results are reproducible
0527             attendee.setUid(QString::number(qHash(attendee.email())));
0528         }
0529         event->addAttendee(attendee, true);
0530     }
0531 
0532     /* According to RFC, only events with attendees can have an organizer.
0533      * Google seems to ignore it, so we must take care of it here */
0534     if (event->attendeeCount() > 0) {
0535         KCalendarCore::Person organizer;
0536         const auto organizerData = data.value(eventOrganizerParam).toMap();
0537         organizer.setName(organizerData.value(organizerDisplayNameParam).toString());
0538         organizer.setEmail(organizerData.value(organizerEmailParam).toString());
0539         event->setOrganizer(organizer);
0540     }
0541 
0542     const QStringList recrs = data.value(eventRecurrenceParam).toStringList();
0543     for (const QString &rec : recrs) {
0544         KCalendarCore::ICalFormat format;
0545         const QStringView recView(rec);
0546         if (recView.left(5) == QLatin1StringView("RRULE")) {
0547             auto recurrenceRule = std::make_unique<KCalendarCore::RecurrenceRule>();
0548             const auto ok = format.fromString(recurrenceRule.get(), rec.mid(6));
0549             Q_UNUSED(ok)
0550             recurrenceRule->setRRule(rec);
0551             event->recurrence()->addRRule(recurrenceRule.release());
0552         } else if (recView.left(6) == QLatin1StringView("EXRULE")) {
0553             auto recurrenceRule = std::make_unique<KCalendarCore::RecurrenceRule>();
0554             const auto ok = format.fromString(recurrenceRule.get(), rec.mid(7));
0555             Q_UNUSED(ok)
0556             recurrenceRule->setRRule(rec);
0557             event->recurrence()->addExRule(recurrenceRule.release());
0558         } else if (recView.left(6) == QLatin1StringView("EXDATE")) {
0559             KCalendarCore::DateList exdates = Private::parseRDate(rec);
0560             event->recurrence()->setExDates(exdates);
0561         } else if (recView.left(5) == QLatin1StringView("RDATE")) {
0562             KCalendarCore::DateList rdates = Private::parseRDate(rec);
0563             event->recurrence()->setRDates(rdates);
0564         }
0565     }
0566 
0567     const auto reminders = data.value(eventRemindersParam).toMap();
0568     if (reminders.contains(reminderUseDefaultParam) && reminders.value(reminderUseDefaultParam).toBool()) {
0569         event->setUseDefaultReminders(true);
0570     } else {
0571         event->setUseDefaultReminders(false);
0572     }
0573 
0574     const auto overrides = reminders.value(reminderOverridesParam).toList();
0575     for (const auto &r : overrides) {
0576         const auto reminderOverride = r.toMap();
0577         auto alarm = KCalendarCore::Alarm::Ptr::create(static_cast<KCalendarCore::Incidence *>(event.data()));
0578         alarm->setTime(event->dtStart());
0579 
0580         if (reminderOverride.value(reminderMethodParam).toString() == popupMethod) {
0581             alarm->setType(KCalendarCore::Alarm::Display);
0582         } else if (reminderOverride.value(reminderMethodParam).toString() == emailMethod) {
0583             alarm->setType(KCalendarCore::Alarm::Email);
0584         } else {
0585             alarm->setType(KCalendarCore::Alarm::Invalid);
0586             continue;
0587         }
0588 
0589         alarm->setStartOffset(KCalendarCore::Duration(reminderOverride.value(reminderMinutesParam).toInt() * (-60)));
0590         alarm->setEnabled(true);
0591         event->addAlarm(alarm);
0592     }
0593 
0594     const auto extendedProperties = data.value(eventExtendedPropertiesParam).toMap();
0595     setEventCategories(event, extendedProperties.value(propertyPrivateParam).toMap());
0596     setEventCategories(event, extendedProperties.value(propertySharedParam).toMap());
0597 
0598     return event.dynamicCast<Object>();
0599 }
0600 
0601 namespace
0602 {
0603 
0604 enum class SerializeDtFlag { AllDay = 1 << 0, IsDtEnd = 1 << 1, HasRecurrence = 1 << 2 };
0605 using SerializeDtFlags = QFlags<SerializeDtFlag>;
0606 
0607 QVariantMap serializeDt(const EventPtr &event, const QDateTime &dt, SerializeDtFlags flags)
0608 {
0609     QVariantMap rv;
0610     if (flags & SerializeDtFlag::AllDay) {
0611         /* For Google, all-day events starts on Monday and ends on Tuesday,
0612          * while in KDE, it both starts and ends on Monday. */
0613         const auto adjusted = dt.addDays((flags & SerializeDtFlag::IsDtEnd) ? 1 : 0);
0614         rv.insert(dateParam, adjusted.toString(QStringLiteral("yyyy-MM-dd")));
0615     } else {
0616         rv.insert(dateTimeParam, Utils::rfc3339DateToString(dt));
0617         QString tzEnd = QString::fromUtf8(dt.timeZone().id());
0618         if (flags & SerializeDtFlag::HasRecurrence && tzEnd.isEmpty()) {
0619             tzEnd = QString::fromUtf8(QTimeZone::utc().id());
0620         }
0621         if (!tzEnd.isEmpty()) {
0622             rv.insert(timeZoneParam, Private::checkAndConverCDOTZID(tzEnd, event));
0623         }
0624     }
0625 
0626     return rv;
0627 }
0628 
0629 } // namespace
0630 
0631 QByteArray eventToJSON(const EventPtr &event, EventSerializeFlags flags)
0632 {
0633     QVariantMap data;
0634 
0635     data.insert(kindParam, eventKind);
0636 
0637     if (!(flags & EventSerializeFlag::NoID)) {
0638         data.insert(idParam, event->id());
0639     }
0640 
0641     data.insert(eventiCalUIDParam, event->uid());
0642 
0643     if (event->status() == KCalendarCore::Incidence::StatusConfirmed) {
0644         data.insert(eventStatusParam, confirmedStatus);
0645     } else if (event->status() == KCalendarCore::Incidence::StatusCanceled) {
0646         data.insert(eventStatusParam, canceledStatus);
0647     } else if (event->status() == KCalendarCore::Incidence::StatusTentative) {
0648         data.insert(eventStatusParam, tentativeStatus);
0649     }
0650 
0651     data.insert(eventSummaryParam, event->summary());
0652     data.insert(eventDescriptionParam, event->description());
0653     data.insert(eventLocationParam, event->location());
0654 
0655     QVariantList recurrence;
0656     KCalendarCore::ICalFormat format;
0657     const auto exRules = event->recurrence()->exRules();
0658     const auto rRules = event->recurrence()->rRules();
0659     recurrence.reserve(rRules.size() + rRules.size() + 2);
0660     for (KCalendarCore::RecurrenceRule *rRule : rRules) {
0661         recurrence.push_back(format.toString(rRule).remove(QStringLiteral("\r\n")));
0662     }
0663     for (KCalendarCore::RecurrenceRule *rRule : exRules) {
0664         recurrence.push_back(format.toString(rRule).remove(QStringLiteral("\r\n")));
0665     }
0666 
0667     QStringList dates;
0668     const auto rDates = event->recurrence()->rDates();
0669     dates.reserve(rDates.size());
0670     for (const auto &rDate : rDates) {
0671         dates.push_back(rDate.toString(QStringLiteral("yyyyMMdd")));
0672     }
0673 
0674     if (!dates.isEmpty()) {
0675         recurrence.push_back(QString(QStringLiteral("RDATE;VALUE=DATA:") + dates.join(QLatin1Char(','))));
0676     }
0677 
0678     dates.clear();
0679     const auto exDates = event->recurrence()->exDates();
0680     dates.reserve(exDates.size());
0681     for (const auto &exDate : exDates) {
0682         dates.push_back(exDate.toString(QStringLiteral("yyyyMMdd")));
0683     }
0684 
0685     if (!dates.isEmpty()) {
0686         recurrence.push_back(QString(QStringLiteral("EXDATE;VALUE=DATE:") + dates.join(QLatin1Char(','))));
0687     }
0688 
0689     if (!recurrence.isEmpty()) {
0690         data.insert(eventRecurrenceParam, recurrence);
0691     }
0692 
0693     SerializeDtFlags dtFlags;
0694     if (event->allDay()) {
0695         dtFlags |= SerializeDtFlag::AllDay;
0696     }
0697     if (!recurrence.isEmpty()) {
0698         dtFlags |= SerializeDtFlag::HasRecurrence;
0699     }
0700 
0701     data.insert(eventStartPram, serializeDt(event, event->dtStart(), dtFlags));
0702     data.insert(eventEndParam, serializeDt(event, event->dtEnd(), dtFlags | SerializeDtFlag::IsDtEnd));
0703 
0704     if (event->hasRecurrenceId()) {
0705         data.insert(eventOrganizerParam, serializeDt(event, event->recurrenceId(), dtFlags));
0706         data.insert(eventRecurringEventIdParam, event->id());
0707     }
0708 
0709     if (event->transparency() == Event::Transparent) {
0710         data.insert(eventTransparencyParam, transparentTransparency);
0711     } else {
0712         data.insert(eventTransparencyParam, opaqueTransparency);
0713     }
0714 
0715     QVariantList atts;
0716     const auto attendees = event->attendees();
0717     for (const auto &attee : attendees) {
0718         QVariantMap att{{attendeeDisplayNameParam, attee.name()}, {attendeeEmailParam, attee.email()}};
0719 
0720         if (attee.status() == KCalendarCore::Attendee::Accepted) {
0721             att.insert(attendeeResponseStatusParam, acceptedStatus);
0722         } else if (attee.status() == KCalendarCore::Attendee::Declined) {
0723             att.insert(attendeeResponseStatusParam, declinedStatus);
0724         } else if (attee.status() == KCalendarCore::Attendee::Tentative) {
0725             att.insert(attendeeResponseStatusParam, tentativeStatus);
0726         } else {
0727             att.insert(attendeeResponseStatusParam, needsActionStatus);
0728         }
0729 
0730         if (attee.role() == KCalendarCore::Attendee::OptParticipant) {
0731             att.insert(attendeeOptionalParam, true);
0732         }
0733         if (!attee.uid().isEmpty()) {
0734             att.insert(idParam, attee.uid());
0735         }
0736         atts.append(att);
0737     }
0738 
0739     if (!atts.isEmpty()) {
0740         data.insert(eventAttendeesParam, atts);
0741 
0742         /* According to RFC, event without attendees should not have
0743          * any organizer. */
0744         const auto organizer = event->organizer();
0745         if (!organizer.isEmpty()) {
0746             data.insert(eventOrganizerParam, QVariantMap{{organizerDisplayNameParam, organizer.fullName()}, {organizerEmailParam, organizer.email()}});
0747         }
0748     }
0749 
0750     QVariantList overrides;
0751     const auto alarms = event->alarms();
0752     for (const auto &alarm : alarms) {
0753         QVariantMap reminderOverride;
0754         if (alarm->type() == KCalendarCore::Alarm::Display) {
0755             reminderOverride.insert(reminderMethodParam, popupMethod);
0756         } else if (alarm->type() == KCalendarCore::Alarm::Email) {
0757             reminderOverride.insert(reminderMethodParam, emailMethod);
0758         } else {
0759             continue;
0760         }
0761         reminderOverride.insert(reminderMinutesParam, (int)(alarm->startOffset().asSeconds() / -60));
0762 
0763         overrides.push_back(reminderOverride);
0764     }
0765 
0766     data.insert(eventRemindersParam, QVariantMap{{reminderUseDefaultParam, false}, {reminderOverridesParam, overrides}});
0767 
0768     if (!event->categories().isEmpty()) {
0769         data.insert(eventExtendedPropertiesParam, QVariantMap{{propertySharedParam, QVariantMap{{categoriesProperty, event->categoriesStr()}}}});
0770     }
0771 
0772     /* TODO: Implement support for additional features:
0773      * https://developers.google.com/gdata/docs/2.0/elements?csw=1
0774      */
0775 
0776     const auto document = QJsonDocument::fromVariant(data);
0777     return document.toJson(QJsonDocument::Compact);
0778 }
0779 
0780 ObjectsList parseEventJSONFeed(const QByteArray &jsonFeed, FeedData &feedData)
0781 {
0782     const auto document = QJsonDocument::fromJson(jsonFeed);
0783     const auto data = document.toVariant().toMap();
0784 
0785     QString timezone;
0786     if (data.value(kindParam) == eventsKind) {
0787         if (data.contains(nextPageTokenParam)) {
0788             QString calendarId = feedData.requestUrl.toString().remove(QStringLiteral("https://www.googleapis.com/calendar/v3/calendars/"));
0789             calendarId = calendarId.left(calendarId.indexOf(QLatin1Char('/')));
0790             feedData.nextPageUrl = feedData.requestUrl;
0791             // replace the old pageToken with a new one
0792             QUrlQuery query(feedData.nextPageUrl);
0793             query.removeQueryItem(pageTokenParam);
0794             query.addQueryItem(pageTokenParam, data.value(nextPageTokenParam).toString());
0795             feedData.nextPageUrl.setQuery(query);
0796         }
0797         if (data.contains(timeZoneParam)) {
0798             // This should always be in Olson format
0799             timezone = data.value(timeZoneParam).toString();
0800         }
0801         if (data.contains(nextSyncTokenParam)) {
0802             feedData.syncToken = data[nextSyncTokenParam].toString();
0803         }
0804     } else {
0805         return {};
0806     }
0807 
0808     ObjectsList list;
0809     const auto items = data.value(itemsParam).toList();
0810     list.reserve(items.size());
0811     for (const auto &i : items) {
0812         list.push_back(Private::JSONToEvent(i.toMap(), timezone));
0813     }
0814 
0815     return list;
0816 }
0817 
0818 /******************************** PRIVATE ***************************************/
0819 
0820 KCalendarCore::DateList Private::parseRDate(const QString &rule)
0821 {
0822     KCalendarCore::DateList list;
0823     QTimeZone tz;
0824     QStringView value;
0825     const auto left = QStringView(rule).left(rule.indexOf(QLatin1Char(':')));
0826 
0827     const auto params = left.split(QLatin1Char(';'));
0828     for (const auto &param : params) {
0829         if (param.startsWith(QLatin1StringView("VALUE"))) {
0830             value = param.mid(param.indexOf(QLatin1Char('=')) + 1);
0831         } else if (param.startsWith(QLatin1StringView("TZID"))) {
0832             auto _name = param.mid(param.indexOf(QLatin1Char('=')) + 1);
0833             tz = QTimeZone(_name.toUtf8());
0834         }
0835     }
0836     const auto datesStr = QStringView(rule).mid(rule.lastIndexOf(QLatin1Char(':')) + 1);
0837     const auto dates = datesStr.split(QLatin1Char(','));
0838     for (const auto &date : dates) {
0839         QDate dt;
0840 
0841         if (value == QLatin1StringView("DATE")) {
0842             dt = QDate::fromString(date.toString(), QStringLiteral("yyyyMMdd"));
0843         } else if (value == QLatin1StringView("PERIOD")) {
0844             const auto start = date.left(date.indexOf(QLatin1Char('/')));
0845             QDateTime kdt = Utils::rfc3339DateFromString(start.toString());
0846             if (tz.isValid()) {
0847                 kdt.setTimeZone(tz);
0848             }
0849 
0850             dt = kdt.date();
0851         } else {
0852             QDateTime kdt = Utils::rfc3339DateFromString(date.toString());
0853             if (tz.isValid()) {
0854                 kdt.setTimeZone(tz);
0855             }
0856 
0857             dt = kdt.date();
0858         }
0859 
0860         list.push_back(dt);
0861     }
0862 
0863     return list;
0864 }
0865 
0866 namespace
0867 {
0868 
0869 /* Based on "Time Zone to CdoTimeZoneId Map"
0870  * https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2007/aa563018(v=exchg.80)
0871  *
0872  * The mapping is not exact, since the CdoTimeZoneId usually refers to a
0873  * region of multiple countries, so I always picked one of the countries
0874  * in the specified region and used it's TZID.
0875  */
0876 static const std::map<int, QLatin1StringView> MSCDOTZIDTable = {
0877     {0, QLatin1StringView("UTC")},
0878     {1, QLatin1StringView("Europe/London")}, /* GMT Greenwich Mean Time; Dublin, Edinburgh, London */
0879     /* Seriously? *sigh* Let's handle these two in checkAndConvertCDOTZID() */
0880     //{2, QLatin1StringView("Europe/Lisbon")},              /* GMT Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London */
0881     //{2, QLatin1StringView("Europe/Sarajevo")},            /* GMT+01:00 Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb */
0882     {3, QLatin1StringView("Europe/Paris")}, /* GMT+01:00 Paris, Madrid, Brussels, Copenhagen */
0883     {4, QLatin1StringView("Europe/Berlin")}, /* GMT+01:00 Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna */
0884     {5, QLatin1StringView("Europe/Bucharest")}, /* GMT+02:00 Bucharest */
0885     {6, QLatin1StringView("Europe/Prague")}, /* GMT+01:00 Prague, Central Europe */
0886     {7, QLatin1StringView("Europe/Athens")}, /* GMT+02:00 Athens, Istanbul, Minsk */
0887     {8, QLatin1StringView("America/Brazil")}, /* GMT-03:00 Brasilia */
0888     {9, QLatin1StringView("America/Halifax")}, /* GMT-04:00 Atlantic time (Canada) */
0889     {10, QLatin1StringView("America/New_York")}, /* GMT-05:00 Eastern Time (US & Canada) */
0890     {11, QLatin1StringView("America/Chicago")}, /* GMT-06:00 Central Time (US & Canada) */
0891     {12, QLatin1StringView("America/Denver")}, /* GMT-07:00 Mountain Time (US & Canada) */
0892     {13, QLatin1StringView("America/Los_Angeles")}, /* GMT-08:00 Pacific Time (US & Canada); Tijuana */
0893     {14, QLatin1StringView("America/Anchorage")}, /* GMT-09:00 Alaska */
0894     {15, QLatin1StringView("Pacific/Honolulu")}, /* GMT-10:00 Hawaii */
0895     {16, QLatin1StringView("Pacific/Apia")}, /* GMT-11:00 Midway Island, Samoa */
0896     {17, QLatin1StringView("Pacific/Auckland")}, /* GMT+12:00 Auckland, Wellington */
0897     {18, QLatin1StringView("Australia/Brisbane")}, /* GMT+10:00 Brisbane, East Australia */
0898     {19, QLatin1StringView("Australia/Adelaide")}, /* GMT+09:30 Adelaide, Central Australia */
0899     {20, QLatin1StringView("Asia/Tokyo")}, /* GMT+09:00 Osaka, Sapporo, Tokyo */
0900     {21, QLatin1StringView("Asia/Singapore")}, /* GMT+08:00 Kuala Lumpur, Singapore */
0901     {22, QLatin1StringView("Asia/Bangkok")}, /* GMT+07:00 Bangkok, Hanoi, Jakarta */
0902     {23, QLatin1StringView("Asia/Calcutta")}, /* GMT+05:30 Kolkata, Chennai, Mumbai, New Delhi, India Standard Time */
0903     {24, QLatin1StringView("Asia/Dubai")}, /* GMT+04:00 Abu Dhabi, Muscat */
0904     {25, QLatin1StringView("Asia/Tehran")}, /* GMT+03:30 Tehran */
0905     {26, QLatin1StringView("Asia/Baghdad")}, /* GMT+03:00 Baghdad */
0906     {27, QLatin1StringView("Asia/Jerusalem")}, /* GMT+02:00 Israel, Jerusalem Standard Time */
0907     {28, QLatin1StringView("America/St_Johns")}, /* GMT-03:30 Newfoundland */
0908     {29, QLatin1StringView("Atlantic/Portugal")}, /* GMT-01:00 Azores */
0909     {30, QLatin1StringView("America/Noronha")}, /* GMT-02:00 Mid-Atlantic */
0910     {31, QLatin1StringView("Africa/Monrovia")}, /* GMT Casablanca, Monrovia */
0911     {32, QLatin1StringView("America/Argentina/Buenos_Aires")}, /* GMT-03:00 Buenos Aires, Georgetown */
0912     {33, QLatin1StringView("America/La_Paz")}, /* GMT-04:00 Caracas, La Paz */
0913     {34, QLatin1StringView("America/New_York")}, /* GMT-05:00 Indiana (East) */
0914     {35, QLatin1StringView("America/Bogota")}, /* GMT-05:00 Bogota, Lima, Quito */
0915     {36, QLatin1StringView("America/Winnipeg")}, /* GMT-06:00 Saskatchewan */
0916     {37, QLatin1StringView("America/Mexico_City")}, /* GMT-06:00 Mexico City, Tegucigalpa */
0917     {38, QLatin1StringView("America/Phoenix")}, /* GMT-07:00 Arizona */
0918     {39, QLatin1StringView("Pacific/Kwajalein")}, /* GMT-12:00 Eniwetok, Kwajalein, Dateline Time */
0919     {40, QLatin1StringView("Pacific/Fiji")}, /* GMT+12:00 Fušál, Kamchatka, Mashall Is. */
0920     {41, QLatin1StringView("Pacific/Noumea")}, /* GMT+11:00 Magadan, Solomon Is., New Caledonia */
0921     {42, QLatin1StringView("Australia/Hobart")}, /* GMT+10:00 Hobart, Tasmania */
0922     {43, QLatin1StringView("Pacific/Guam")}, /* GMT+10:00 Guam, Port Moresby */
0923     {44, QLatin1StringView("Australia/Darwin")}, /* GMT+09:30 Darwin */
0924     {45, QLatin1StringView("Asia/Shanghai")}, /* GMT+08:00 Beijing, Chongqing, Hong Kong SAR, Urumqi */
0925     {46, QLatin1StringView("Asia/Omsk")}, /* GMT+06:00 Almaty, Novosibirsk, North Central Asia */
0926     {47, QLatin1StringView("Asia/Karachi")}, /* GMT+05:00 Islamabad, Karachi, Tashkent */
0927     {48, QLatin1StringView("Asia/Kabul")}, /* GMT+04:30 Kabul */
0928     {49, QLatin1StringView("Africa/Cairo")}, /* GMT+02:00 Cairo */
0929     {50, QLatin1StringView("Africa/Harare")}, /* GMT+02:00 Harare, Pretoria */
0930     {51, QLatin1StringView("Europe/Moscow")}, /* GMT+03:00 Moscow, St. Petersburg, Volgograd */
0931     {53, QLatin1StringView("Atlantic/Cape_Verde")}, /* GMT-01:00 Cape Verde Is. */
0932     {54, QLatin1StringView("Asia/Tbilisi")}, /* GMT+04:00 Baku, Tbilisi, Yerevan */
0933     {55, QLatin1StringView("America/Tegucigalpa")}, /* GMT-06:00 Central America */
0934     {56, QLatin1StringView("Africa/Nairobi")}, /* GMT+03:00 East Africa, Nairobi */
0935     {58, QLatin1StringView("Asia/Yekaterinburg")}, /* GMT+05:00 Ekaterinburg */
0936     {59, QLatin1StringView("Europe/Helsinki")}, /* GMT+02:00 Helsinki, Riga, Tallinn */
0937     {60, QLatin1StringView("America/Greenland")}, /* GMT-03:00 Greenland */
0938     {61, QLatin1StringView("Asia/Rangoon")}, /* GMT+06:30 Yangon (Rangoon) */
0939     {62, QLatin1StringView("Asia/Katmandu")}, /* GMT+05:45 Kathmandu, Nepal */
0940     {63, QLatin1StringView("Asia/Irkutsk")}, /* GMT+08:00 Irkutsk, Ulaan Bataar */
0941     {64, QLatin1StringView("Asia/Krasnoyarsk")}, /* GMT+07:00 Krasnoyarsk */
0942     {65, QLatin1StringView("America/Santiago")}, /* GMT-04:00 Santiago */
0943     {66, QLatin1StringView("Asia/Colombo")}, /* GMT+06:00 Sri Jayawardenepura, Sri Lanka */
0944     {67, QLatin1StringView("Pacific/Tongatapu")}, /* GMT+13:00 Nuku'alofa, Tonga */
0945     {68, QLatin1StringView("Asia/Vladivostok")}, /* GMT+10:00 Vladivostok */
0946     {69, QLatin1StringView("Africa/Bangui")}, /* GMT+01:00 West Central Africa */
0947     {70, QLatin1StringView("Asia/Yakutsk")}, /* GMT+09:00 Yakutsk */
0948     {71, QLatin1StringView("Asia/Dhaka")}, /* GMT+06:00 Astana, Dhaka */
0949     {72, QLatin1StringView("Asia/Seoul")}, /* GMT+09:00 Seoul, Korea Standard time */
0950     {73, QLatin1StringView("Australia/Perth")}, /* GMT+08:00 Perth, Western Australia */
0951     {74, QLatin1StringView("Asia/Kuwait")}, /* GMT+03:00 Arab, Kuwait, Riyadh */
0952     {75, QLatin1StringView("Asia/Taipei")}, /* GMT+08:00 Taipei */
0953     {76, QLatin1StringView("Australia/Sydney")} /* GMT+10:00 Canberra, Melbourne, Sydney */
0954 };
0955 
0956 /* Based on "Microsoft Time Zone Index Values"
0957  * https://support.microsoft.com/en-gb/help/973627/microsoft-time-zone-index-values
0958  *
0959  * The mapping is not exact, since the TZID usually refers to a
0960  * region of multiple countries, so I always picked one of the countries
0961  * in the specified region and used it's TZID.
0962  *
0963  * The Olson timezones are taken from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
0964  *
0965  * Note: using std::map, because it allows heterogeneous lookup, i.e. I can lookup the QLatin1StringView
0966  * keys by using QString value, which is not possible with Qt containers.
0967  */
0968 static const std::map<QLatin1StringView, QLatin1StringView, std::less<>> MSSTTZTable = {
0969     {QLatin1StringView("Dateline Standard Time"), QLatin1StringView("Pacific/Kwajalein")}, /* (GMT-12:00) International Date Line West */
0970     {QLatin1StringView("Samoa Standard Time"), QLatin1StringView("Pacific/Apia")}, /* (GMT-11:00) Midway Island, Samoa */
0971     {QLatin1StringView("Hawaiian Standard Time"), QLatin1StringView("Pacific/Honolulu")}, /* (GMT-10:00) Hawaii */
0972     {QLatin1StringView("Alaskan Standard Time"), QLatin1StringView("America/Anchorage")}, /* (GMT-09:00) Alaska */
0973     {QLatin1StringView("Pacific Standard Time"), QLatin1StringView("America/Los_Angeles")}, /* (GMT-08:00) Pacific Time (US and Canada); Tijuana */
0974     {QLatin1StringView("Mountain Standard Time"), QLatin1StringView("America/Denver")}, /* (GMT-07:00) Mountain Time (US and Canada) */
0975     {QLatin1StringView("Mexico Standard Time 2"), QLatin1StringView("America/Chihuahua")}, /* (GMT-07:00) Chihuahua, La Paz, Mazatlan */
0976     {QLatin1StringView("U.S. Mountain Standard Time"), QLatin1StringView("America/Phoenix")}, /* (GMT-07:00) Arizona */
0977     {QLatin1StringView("Central Standard Time"), QLatin1StringView("America/Chicago")}, /* (GMT-06:00) Central Time (US and Canada */
0978     {QLatin1StringView("Canada Central Standard Time"), QLatin1StringView("America/Winnipeg")}, /* (GMT-06:00) Saskatchewan */
0979     {QLatin1StringView("Mexico Standard Time"), QLatin1StringView("America/Mexico_City")}, /* (GMT-06:00) Guadalajara, Mexico City, Monterrey */
0980     {QLatin1StringView("Central America Standard Time"), QLatin1StringView("America/Chicago")}, /* (GMT-06:00) Central America */
0981     {QLatin1StringView("Eastern Standard Time"), QLatin1StringView("America/New_York")}, /* (GMT-05:00) Eastern Time (US and Canada) */
0982     {QLatin1StringView("U.S. Eastern Standard Time"), QLatin1StringView("America/New_York")}, /* (GMT-05:00) Indiana (East) */
0983     {QLatin1StringView("S.A. Pacific Standard Time"), QLatin1StringView("America/Bogota")}, /* (GMT-05:00) Bogota, Lima, Quito */
0984     {QLatin1StringView("Atlantic Standard Time"), QLatin1StringView("America/Halifax")}, /* (GMT-04:00) Atlantic Time (Canada) */
0985     {QLatin1StringView("S.A. Western Standard Time"), QLatin1StringView("America/La_Paz")}, /* (GMT-04:00) Caracas, La Paz */
0986     {QLatin1StringView("Pacific S.A. Standard Time"), QLatin1StringView("America/Santiago")}, /* (GMT-04:00) Santiago */
0987     {QLatin1StringView("Newfoundland and Labrador Standard Time"), QLatin1StringView("America/St_Johns")}, /* (GMT-03:30) Newfoundland and Labrador */
0988     {QLatin1StringView("E. South America Standard Time"), QLatin1StringView("America/Brazil")}, /* (GMT-03:00) Brasilia */
0989     {QLatin1StringView("S.A. Eastern Standard Time"), QLatin1StringView("America/Argentina/Buenos_Aires")}, /* (GMT-03:00) Buenos Aires, Georgetown */
0990     {QLatin1StringView("Greenland Standard Time"), QLatin1StringView("America/Greenland")}, /* (GMT-03:00) Greenland */
0991     {QLatin1StringView("Mid-Atlantic Standard Time"), QLatin1StringView("America/Noronha")}, /* (GMT-02:00) Mid-Atlantic */
0992     {QLatin1StringView("Azores Standard Time"), QLatin1StringView("Atlantic/Portugal")}, /* (GMT-01:00) Azores */
0993     {QLatin1StringView("Cape Verde Standard Time"), QLatin1StringView("Atlantic/Cape_Verde")}, /* (GMT-01:00) Cape Verde Islands */
0994     {QLatin1StringView("GMT Standard Time"), QLatin1StringView("Europe/London")}, /* (GMT) Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London */
0995     {QLatin1StringView("Greenwich Standard Time"), QLatin1StringView("Africa/Casablanca")}, /* (GMT) Casablanca, Monrovia */
0996     {QLatin1StringView("Central Europe Standard Time"), QLatin1StringView("Europe/Prague")}, /* (GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague */
0997     {QLatin1StringView("Central European Standard Time"), QLatin1StringView("Europe/Sarajevo")}, /* (GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb */
0998     {QLatin1StringView("Romance Standard Time"), QLatin1StringView("Europe/Brussels")}, /* (GMT+01:00) Brussels, Copenhagen, Madrid, Paris */
0999     {QLatin1StringView("W. Europe Standard Time"), QLatin1StringView("Europe/Amsterdam")}, /* (GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna */
1000     {QLatin1StringView("W. Central Africa Standard Time"), QLatin1StringView("Africa/Bangui")}, /* (GMT+01:00) West Central Africa */
1001     {QLatin1StringView("E. Europe Standard Time"), QLatin1StringView("Europe/Bucharest")}, /* (GMT+02:00) Bucharest */
1002     {QLatin1StringView("Egypt Standard Time"), QLatin1StringView("Africa/Cairo")}, /* (GMT+02:00) Cairo */
1003     {QLatin1StringView("FLE Standard Time"), QLatin1StringView("Europe/Helsinki")}, /* (GMT+02:00) Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius */
1004     {QLatin1StringView("GTB Standard Time"), QLatin1StringView("Europe/Athens")}, /* (GMT+02:00) Athens, Istanbul, Minsk */
1005     {QLatin1StringView("Israel Standard Time"), QLatin1StringView("Europe/Athens")}, /* (GMT+02:00) Jerusalem */
1006     {QLatin1StringView("South Africa Standard Time"), QLatin1StringView("Africa/Harare")}, /* (GMT+02:00) Harare, Pretoria */
1007     {QLatin1StringView("Russian Standard Time"), QLatin1StringView("Europe/Moscow")}, /* (GMT+03:00) Moscow, St. Petersburg, Volgograd */
1008     {QLatin1StringView("Arab Standard Time"), QLatin1StringView("Asia/Kuwait")}, /* (GMT+03:00) Kuwait, Riyadh */
1009     {QLatin1StringView("E. Africa Standard Time"), QLatin1StringView("Africa/Nairobi")}, /* (GMT+03:00) Nairobi */
1010     {QLatin1StringView("Arabic Standard Time"), QLatin1StringView("Asia/Baghdad")}, /* (GMT+03:00) Baghdad */
1011     {QLatin1StringView("Iran Standard Time"), QLatin1StringView("Asia/Tehran")}, /* (GMT+03:30) Tehran */
1012     {QLatin1StringView("Arabian Standard Time"), QLatin1StringView("Asia/Dubai")}, /* (GMT+04:00) Abu Dhabi, Muscat */
1013     {QLatin1StringView("Caucasus Standard Time"), QLatin1StringView("Asia/Tbilisi")}, /* (GMT+04:00) Baku, Tbilisi, Yerevan */
1014     {QLatin1StringView("Transitional Islamic State of Afghanistan Standard Time"), QLatin1StringView("Asia/Kabul")}, /* (GMT+04:30) Kabul */
1015     {QLatin1StringView("Ekaterinburg Standard Time"), QLatin1StringView("Asia/Yekaterinburg")}, /* (GMT+05:00) Ekaterinburg */
1016     {QLatin1StringView("West Asia Standard Time"), QLatin1StringView("Asia/Karachi")}, /* (GMT+05:00) Islamabad, Karachi, Tashkent */
1017     {QLatin1StringView("India Standard Time"), QLatin1StringView("Asia/Calcutta")}, /* (GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi */
1018     {QLatin1StringView("Nepal Standard Time"), QLatin1StringView("Asia/Calcutta")}, /* (GMT+05:45) Kathmandu */
1019     {QLatin1StringView("Central Asia Standard Time"), QLatin1StringView("Asia/Dhaka")}, /* (GMT+06:00) Astana, Dhaka */
1020     {QLatin1StringView("Sri Lanka Standard Time"), QLatin1StringView("Asia/Colombo")}, /* (GMT+06:00) Sri Jayawardenepura */
1021     {QLatin1StringView("N. Central Asia Standard Time"), QLatin1StringView("Asia/Omsk")}, /* (GMT+06:00) Almaty, Novosibirsk */
1022     {QLatin1StringView("Myanmar Standard Time"), QLatin1StringView("Asia/Rangoon")}, /* (GMT+06:30) Yangon Rangoon */
1023     {QLatin1StringView("S.E. Asia Standard Time"), QLatin1StringView("Asia/Bangkok")}, /* (GMT+07:00) Bangkok, Hanoi, Jakarta */
1024     {QLatin1StringView("North Asia Standard Time"), QLatin1StringView("Asia/Krasnoyarsk")}, /* (GMT+07:00) Krasnoyarsk */
1025     {QLatin1StringView("China Standard Time"), QLatin1StringView("Asia/Shanghai")}, /* (GMT+08:00) Beijing, Chongqing, Hong Kong SAR, Urumqi */
1026     {QLatin1StringView("Singapore Standard Time"), QLatin1StringView("Asia/Singapore")}, /* (GMT+08:00) Kuala Lumpur, Singapore */
1027     {QLatin1StringView("Taipei Standard Time"), QLatin1StringView("Asia/Taipei")}, /* (GMT+08:00) Taipei */
1028     {QLatin1StringView("W. Australia Standard Time"), QLatin1StringView("Australia/Perth")}, /* (GMT+08:00) Perth */
1029     {QLatin1StringView("North Asia East Standard Time"), QLatin1StringView("Asia/Irkutsk")}, /* (GMT+08:00) Irkutsk, Ulaanbaatar */
1030     {QLatin1StringView("Korea Standard Time"), QLatin1StringView("Asia/Seoul")}, /* (GMT+09:00) Seoul */
1031     {QLatin1StringView("Tokyo Standard Time"), QLatin1StringView("Asia/Tokyo")}, /* (GMT+09:00) Osaka, Sapporo, Tokyo */
1032     {QLatin1StringView("Yakutsk Standard Time"), QLatin1StringView("Asia/Yakutsk")}, /* (GMT+09:00) Yakutsk */
1033     {QLatin1StringView("A.U.S. Central Standard Time"), QLatin1StringView("Australia/Darwin")}, /* (GMT+09:30) Darwin */
1034     {QLatin1StringView("Cen. Australia Standard Time"), QLatin1StringView("Australia/Adelaide")}, /* (GMT+09:30) Adelaide */
1035     {QLatin1StringView("A.U.S. Eastern Standard Time"), QLatin1StringView("Australia/Sydney")}, /* (GMT+10:00) Canberra, Melbourne, Sydney */
1036     {QLatin1StringView("E. Australia Standard Time"), QLatin1StringView("Australia/Brisbane")}, /* (GMT+10:00) Brisbane */
1037     {QLatin1StringView("Tasmania Standard Time"), QLatin1StringView("Australia/Hobart")}, /* (GMT+10:00) Hobart */
1038     {QLatin1StringView("Vladivostok Standard Time"), QLatin1StringView("Asia/Vladivostok")}, /* (GMT+10:00) Vladivostok */
1039     {QLatin1StringView("West Pacific Standard Time"), QLatin1StringView("Pacific/Guam")}, /* (GMT+10:00) Guam, Port Moresby */
1040     {QLatin1StringView("Central Pacific Standard Time"), QLatin1StringView("Pacific/Noumea")}, /* (GMT+11:00) Magadan, Solomon Islands, New Caledonia */
1041     {QLatin1StringView("Fiji Islands Standard Time"), QLatin1StringView("Pacific/Fiji")}, /* (GMT+12:00) Fiji Islands, Kamchatka, Marshall Islands */
1042     {QLatin1StringView("New Zealand Standard Time"), QLatin1StringView("Pacific/Auckland")}, /* (GMT+12:00) Auckland, Wellington */
1043     {QLatin1StringView("Tonga Standard Time"), QLatin1StringView("Pacific/Tongatapu")}, /* (GMT+13:00) Nuku'alofa */
1044     {QLatin1StringView("Azerbaijan Standard Time"), QLatin1StringView("America/Argentina/Buenos_Aires")}, /* (GMT-03:00) Buenos Aires */
1045     {QLatin1StringView("Middle East Standard Time"), QLatin1StringView("Asia/Beirut")}, /* (GMT+02:00) Beirut */
1046     {QLatin1StringView("Jordan Standard Time"), QLatin1StringView("Asia/Amman")}, /* (GMT+02:00) Amman */
1047     {QLatin1StringView("Central Standard Time (Mexico)"), QLatin1StringView("America/Mexico_City")}, /* (GMT-06:00) Guadalajara, Mexico City, Monterrey - New */
1048     {QLatin1StringView("Mountain Standard Time (Mexico)"), QLatin1StringView("America/Ojinaga")}, /* (GMT-07:00) Chihuahua, La Paz, Mazatlan - New */
1049     {QLatin1StringView("Pacific Standard Time (Mexico)"), QLatin1StringView("America/Tijuana")}, /* (GMT-08:00) Tijuana, Baja California */
1050     {QLatin1StringView("Namibia Standard Time"), QLatin1StringView("Africa/Windhoek")}, /* (GMT+02:00) Windhoek */
1051     {QLatin1StringView("Georgian Standard Time"), QLatin1StringView("Asia/Tbilisi")}, /* (GMT+03:00) Tbilisi */
1052     {QLatin1StringView("Central Brazilian Standard Time"), QLatin1StringView("America/Manaus")}, /*(GMT-04:00) Manaus */
1053     {QLatin1StringView("Montevideo Standard Time"), QLatin1StringView("America/Montevideo")}, /* (GMT-03:00) Montevideo */
1054     {QLatin1StringView("Armenian Standard Time"), QLatin1StringView("Asia/Yerevan")}, /* (GMT+04:00) Yerevan */
1055     {QLatin1StringView("Venezuela Standard Time"), QLatin1StringView("America/Caracas")}, /* (GMT-04:30) Caracas */
1056     {QLatin1StringView("Argentina Standard Time"), QLatin1StringView("America/Argentina/Buenos_Aires")}, /* (GMT-03:00) Buenos Aires */
1057     {QLatin1StringView("Morocco Standard Time"), QLatin1StringView("Africa/Casablanca")}, /* (GMT) Casablanca */
1058     {QLatin1StringView("Pakistan Standard Time"), QLatin1StringView("Asia/Karachi")}, /* (GMT+05:00) Islamabad, Karachi */
1059     {QLatin1StringView("Mauritius Standard Time"), QLatin1StringView("Indian/Mauritius")}, /* (GMT+04:00) Port Louis */
1060     {QLatin1StringView("UTC"), QLatin1StringView("UTC")}, /* (GMT) Coordinated Universal Time */
1061     {QLatin1StringView("Paraguay Standard Time"), QLatin1StringView("America/Asuncion")}, /* (GMT-04:00) Asuncion */
1062     {QLatin1StringView("Kamchatka Standard Time"), QLatin1StringView("Asia/Kamchatka")}, /* (GMT+12:00) Petropavlovsk-Kamchatsky */
1063 };
1064 } // namespace
1065 
1066 QString Private::checkAndConverCDOTZID(const QString &tzid, const EventPtr &event)
1067 {
1068     /* Try to match the @tzid to any valid timezone we know. */
1069     QTimeZone tz(tzid.toLatin1());
1070     if (tz.isValid()) {
1071         /* Yay, @tzid is a valid TZID in Olson format */
1072         return tzid;
1073     }
1074 
1075     /* Damn, no match. Parse the iCal and try to find X-MICROSOFT-CDO-TZID
1076      * property that we can match against the MSCDOTZIDTable */
1077     KCalendarCore::ICalFormat format;
1078     /* Use a copy of @event, otherwise it would be deleted when ptr is destroyed */
1079     KCalendarCore::Incidence::Ptr incidence = event.dynamicCast<KCalendarCore::Incidence>();
1080     const QString vcard = format.toICalString(incidence);
1081     const QStringList properties = vcard.split(QLatin1Char('\n'));
1082     int CDOId = -1;
1083     for (const QString &property : properties) {
1084         if (property.startsWith(u"X-MICROSOFT-CDO-TZID")) {
1085             QStringList parsed = property.split(QLatin1Char(':'));
1086             if (parsed.length() != 2) {
1087                 break;
1088             }
1089 
1090             CDOId = parsed.at(1).toInt();
1091             break;
1092         }
1093     }
1094 
1095     /* Wheeee, we have X-MICROSOFT-CDO-TZID, try to map it to Olson format */
1096     if (CDOId > -1) {
1097         /* *sigh* Some expert in MS assigned the same ID to two different timezones... */
1098         if (CDOId == 2) {
1099             /* GMT Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London */
1100             if (tzid.contains(QLatin1StringView("Dublin")) || tzid.contains(QLatin1StringView("Edinburgh")) || tzid.contains(QLatin1StringView("Lisbon"))
1101                 || tzid.contains(QLatin1StringView("London"))) {
1102                 return QStringLiteral("Europe/London");
1103             }
1104 
1105             /* GMT+01:00 Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb */
1106             else if (tzid.contains(QLatin1StringView("Sarajevo")) || tzid.contains(QLatin1StringView("Skopje")) || tzid.contains(QLatin1StringView("Sofija"))
1107                      || tzid.contains(QLatin1StringView("Vilnius")) || tzid.contains(QLatin1StringView("Warsaw")) || tzid.contains(QLatin1StringView("Zagreb"))) {
1108                 return QStringLiteral("Europe/Sarajevo");
1109             }
1110         }
1111 
1112         const auto it = MSCDOTZIDTable.find(CDOId);
1113         if (it != MSCDOTZIDTable.cend()) {
1114             return it->second;
1115         }
1116     }
1117 
1118     /* We failed to map to X-MICROSOFT-CDO-TZID. Let's try mapping the TZID
1119      * onto the Microsoft Standard Time Zones */
1120     const auto it = MSSTTZTable.find(tzid);
1121     if (it != MSSTTZTable.cend()) {
1122         return it->second;
1123     }
1124 
1125     /* Fail/ Just return the original TZID and hope Google will accept it
1126      * (though we know it won't) */
1127     return tzid;
1128 }
1129 
1130 } // namespace CalendarService
1131 
1132 } // namespace KGAPI2