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 ¶m : 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