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