1 /*
2  * SPDX-FileCopyrightText: 2020 Dimitris Kardarakos <dimkard@posteo.net>
3  *
4  * SPDX-License-Identifier: GPL-3.0-or-later
5  */
6 
7 #include "calendarcontroller.h"
8 #include "localcalendar.h"
9 #include <QStandardPaths>
10 #include <QDebug>
11 #include <QFile>
12 #include <QStringLiteral>
13 #include <KLocalizedString>
14 #include <KCalendarCore/ICalFormat>
15 #include <KCalendarCore/Calendar>
16 #include <KCalendarCore/MemoryCalendar>
17 #include <KCalendarCore/Attendee>
18 #include <KCalendarCore/Person>
19 #include "attendeesmodel.h"
20 #include "calindoriconfig.h"
21 
CalendarController(QObject * parent)22 CalendarController::CalendarController(QObject *parent) : QObject {parent}, m_events {}, m_todos {}
23 {
24 }
25 
importCalendarData(const QByteArray & data)26 void CalendarController::importCalendarData(const QByteArray &data)
27 {
28     KCalendarCore::MemoryCalendar::Ptr importedCalendar {new KCalendarCore::MemoryCalendar {QTimeZone::systemTimeZoneId()}};
29     KCalendarCore::ICalFormat icalFormat {};
30     auto readResult = icalFormat.fromRawString(importedCalendar, data);
31 
32     if (!readResult) {
33         qDebug() << "The file read was not a valid calendar";
34         Q_EMIT statusMessageChanged(i18n("The url or file given does not contain valid calendar data"), MessageType::NegativeAnswer);
35         return;
36     }
37 
38     m_events = importedCalendar->rawEvents();
39     m_todos = importedCalendar->rawTodos();
40 
41     if (m_events.isEmpty() && m_todos.isEmpty()) {
42         qDebug() << "No events or tasks found.";
43         Q_EMIT statusMessageChanged(i18n("The url or file given does not contain any event or task"), MessageType::NegativeAnswer);
44         return;
45     }
46 
47     QString confirmMsg {};
48 
49     if (!m_events.isEmpty() && m_todos.isEmpty()) {
50         confirmMsg = i18np("1 event will be added", "%1 events will be added", m_events.count());
51     } else if (m_events.isEmpty() && !m_todos.isEmpty()) {
52         confirmMsg = i18np("1 task will be added", "%1 tasks will be added", m_todos.count());
53     } else {
54         auto incidenceCount = m_events.count() + m_todos.count();
55         confirmMsg = i18n("%1 incidences will be added", incidenceCount);
56     }
57 
58     Q_EMIT statusMessageChanged(confirmMsg, MessageType::Question);
59 }
60 
importFromBuffer(LocalCalendar * localCalendar)61 void CalendarController::importFromBuffer(LocalCalendar *localCalendar)
62 {
63     auto calendar = localCalendar->calendar();
64 
65     for (const auto &event : qAsConst(m_events)) {
66         calendar->addEvent(event);
67     }
68 
69     for (const auto &todo : qAsConst(m_todos)) {
70         calendar->addTodo(todo);
71     }
72 
73     bool result = localCalendar->save();
74 
75     if (!m_events.isEmpty()) {
76         Q_EMIT localCalendar->eventsChanged();
77         m_events.clear();
78     }
79 
80     if (!m_todos.isEmpty()) {
81         Q_EMIT localCalendar->todosChanged();
82         m_todos.clear();
83     }
84 
85     sendMessage(result);
86 }
87 
importFromBuffer(const QString & targetCalendar)88 void CalendarController::importFromBuffer(const QString &targetCalendar)
89 {
90     auto filePath = CalindoriConfig {}.calendarFile(targetCalendar);
91     QFile calendarFile {filePath};
92     if (!calendarFile.exists()) {
93         sendMessage(false);
94         return;
95     }
96 
97     Calendar::Ptr calendar {new MemoryCalendar(QTimeZone::systemTimeZoneId())};
98     FileStorage::Ptr storage {new FileStorage {calendar}};
99     storage->setFileName(filePath);
100     if (!storage->load()) {
101         sendMessage(false);
102         return;
103     }
104 
105     for (const auto &event : qAsConst(m_events)) {
106         calendar->addEvent(event);
107     }
108 
109     for (const auto &todo : qAsConst(m_todos)) {
110         calendar->addTodo(todo);
111     }
112 
113     sendMessage(storage->save());
114 }
115 
sendMessage(const bool positive)116 void CalendarController::sendMessage(const bool positive)
117 {
118     if (positive) {
119         Q_EMIT statusMessageChanged(i18n("Import completed successfully"), MessageType::PositiveAnswer);
120     } else {
121         Q_EMIT statusMessageChanged(i18n("An error has occurred during import"), MessageType::NegativeAnswer);
122     }
123 }
124 
abortImporting()125 void CalendarController::abortImporting()
126 {
127     m_events.clear();
128     m_todos.clear();
129 }
130 
removeEvent(LocalCalendar * localCalendar,const QVariantMap & eventData)131 void CalendarController::removeEvent(LocalCalendar *localCalendar, const QVariantMap &eventData)
132 {
133     Calendar::Ptr calendar = localCalendar->calendar();
134     QString uid = eventData["uid"].toString();
135     Event::Ptr event = calendar->event(uid);
136     calendar->deleteEvent(event);
137     bool deleted = localCalendar->save();
138     Q_EMIT localCalendar->eventsChanged();
139 
140     qDebug() << "Event " << uid << " deleted: " << deleted;
141 }
142 
upsertEvent(LocalCalendar * localCalendar,const QVariantMap & eventData,const QVariantList & attendeesList)143 void CalendarController::upsertEvent(LocalCalendar *localCalendar, const QVariantMap &eventData, const QVariantList &attendeesList)
144 {
145     qDebug() << "\naddEdit:\tCreating event";
146 
147     Calendar::Ptr calendar = localCalendar->calendar();
148     QDateTime now = QDateTime::currentDateTime();
149     QString uid = eventData["uid"].toString();
150     QString summary = eventData["summary"].toString();
151     bool clearPartStatus {false};
152 
153     QDate startDate = eventData["startDate"].toDate();
154     int startHour = eventData["startHour"].value<int>();
155     int startMinute = eventData["startMinute"].value<int>();
156 
157     QDate endDate = eventData["endDate"].toDate();
158     int endHour = eventData["endHour"].value<int>();
159     int endMinute = eventData["endMinute"].value<int>();
160 
161     QDateTime startDateTime;
162     QDateTime endDateTime;
163     bool allDayFlg = eventData["allDay"].toBool();
164 
165     if (allDayFlg) {
166         startDateTime = startDate.startOfDay();
167         endDateTime = endDate.startOfDay();
168     } else {
169         startDateTime = QDateTime(startDate, QTime(startHour, startMinute, 0, 0), QTimeZone::systemTimeZone());
170         endDateTime = QDateTime(endDate, QTime(endHour, endMinute, 0, 0), QTimeZone::systemTimeZone());
171     }
172 
173     Event::Ptr event;
174     if (uid == "") {
175         event = Event::Ptr(new Event());
176         event->setUid(summary.at(0) + now.toString("yyyyMMddhhmmsszzz"));
177     } else {
178         event = calendar->event(uid);
179         event->setUid(uid);
180         clearPartStatus = (event->dtStart() != startDateTime) || (event->dtEnd() != endDateTime) || (event->allDay() != allDayFlg);
181     }
182 
183     event->setDtStart(startDateTime);
184     event->setDtEnd(endDateTime);
185     event->setDescription(eventData["description"].toString());
186     event->setSummary(summary);
187     event->setAllDay(allDayFlg);
188     event->setLocation(eventData["location"].toString());
189 
190     event->clearAttendees();
191     for (auto &a : qAsConst(attendeesList)) {
192         auto attendee = a.value<KCalendarCore::Attendee>();
193         if (clearPartStatus) {
194             qDebug() << "Participants need to be informed";
195             attendee.setRSVP(true);
196             attendee.setStatus(KCalendarCore::Attendee::PartStat::NeedsAction);
197         }
198         event->addAttendee(attendee);
199     }
200 
201     if (!attendeesList.isEmpty()) {
202         event->setOrganizer(KCalendarCore::Person { localCalendar->ownerName(), localCalendar->ownerEmail()});
203     }
204 
205     event->clearAlarms();
206     QVariantList newAlarms = eventData["alarms"].value<QVariantList>();
207     QVariantList::const_iterator itr = newAlarms.constBegin();
208     while (itr != newAlarms.constEnd()) {
209         Alarm::Ptr newAlarm = event->newAlarm();
210         QHash<QString, QVariant> newAlarmHashMap = (*itr).value<QHash<QString, QVariant>>();
211         int startOffsetValue = newAlarmHashMap["startOffsetValue"].value<int>();
212         int startOffsetType = newAlarmHashMap["startOffsetType"].value<int>();
213         int actionType = newAlarmHashMap["actionType"].value<int>();
214 
215         qDebug() << "addEdit:\tAdding alarm with start offset value " << startOffsetValue;
216         newAlarm->setStartOffset(Duration(startOffsetValue, static_cast<Duration::Type>(startOffsetType)));
217         newAlarm->setType(static_cast<Alarm::Type>(actionType));
218         newAlarm->setEnabled(true);
219         newAlarm->setText((event->summary()).isEmpty() ?  event->description() : event->summary());
220         ++itr;
221     }
222 
223     ushort newPeriod = static_cast<ushort>(eventData["periodType"].toInt());
224 
225     //Bother with recurrences only if a recurrence has been found, either existing or new
226     if ((event->recurrenceType() != Recurrence::rNone) || (newPeriod != Recurrence::rNone)) {
227         //WORKAROUND: When changing an event from non-recurring to recurring, duplicate events are displayed
228         if (uid != "") {
229             calendar->deleteEvent(event);
230         }
231 
232         switch (newPeriod) {
233         case Recurrence::rYearlyDay:
234         case Recurrence::rYearlyMonth:
235         case Recurrence::rYearlyPos:
236             event->recurrence()->setYearly(eventData["repeatEvery"].toInt());
237             break;
238         case Recurrence::rMonthlyDay:
239         case Recurrence::rMonthlyPos:
240             event->recurrence()->setMonthly(eventData["repeatEvery"].toInt());
241             break;
242         case Recurrence::rWeekly:
243             event->recurrence()->setWeekly(eventData["repeatEvery"].toInt());
244             break;
245         case Recurrence::rDaily:
246             event->recurrence()->setDaily(eventData["repeatEvery"].toInt());
247             break;
248         default:
249             event->recurrence()->clear();
250         }
251 
252         if (newPeriod != Recurrence::rNone) {
253             int stopAfter = eventData["stopAfter"].toInt() > 0 ? eventData["stopAfter"].toInt() : -1;
254             event->recurrence()->setDuration(stopAfter);
255             event->recurrence()->setAllDay(allDayFlg);
256         }
257 
258         if (uid != "") {
259             calendar->addEvent(event);
260         }
261     }
262 
263     event->setStatus(eventData["status"].value<KCalendarCore::Incidence::Status>());
264 
265     if (uid == "") {
266         calendar->addEvent(event);
267     }
268 
269     bool merged = localCalendar->save();
270     Q_EMIT localCalendar->eventsChanged();
271 
272     qDebug() << "Event upsert: " << merged;
273 }
274 
localSystemDateTime() const275 QDateTime CalendarController::localSystemDateTime() const
276 {
277     return QDateTime::currentDateTime();
278 }
279 
validateEvent(const QVariantMap & eventMap) const280 QVariantMap CalendarController::validateEvent(const QVariantMap &eventMap) const
281 {
282     QVariantMap result {};
283 
284     QDate startDate = eventMap["startDate"].toDate();
285     bool validStartHour {false};
286     int startHour = eventMap["startHour"].toInt(&validStartHour);
287     bool validStartMinutes {false};
288     int startMinute = eventMap["startMinute"].toInt(&validStartMinutes);
289     QDate endDate = eventMap["endDate"].toDate();
290     bool validEndHour {false};
291     int endHour = eventMap["endHour"].toInt(&validEndHour);
292     bool validEndMinutes {false};
293     int endMinutes = eventMap["endMinute"].toInt(&validEndMinutes);
294     bool allDayFlg = eventMap["allDay"].toBool();
295 
296     if (startDate.isValid() && validStartHour && validStartMinutes && endDate.isValid() && validEndHour && validEndMinutes) {
297         if (allDayFlg && (endDate != startDate)) {
298             result["success"] = false;
299             result["reason"] = i18n("In case of all day events, start date and end date should be equal");
300 
301             return result;
302         }
303 
304         auto startDateTime = QDateTime(startDate, QTime(startHour, startMinute, 0, 0), QTimeZone::systemTimeZone());
305         auto endDateTime = QDateTime(endDate, QTime(endHour, endMinutes, 0, 0), QTimeZone::systemTimeZone());
306 
307         if (!allDayFlg && (startDateTime > endDateTime)) {
308             result["success"] = false;
309             result["reason"] = i18n("End date time should be equal to or greater than the start date time");
310             return result;
311         }
312 
313         auto validPeriodType {false};
314         auto periodType = static_cast<ushort>(eventMap["periodType"].toInt(&validPeriodType));
315         auto eventDuration = startDateTime.secsTo(endDateTime);
316         auto validRepeatEvery {false};
317         auto repeatEvery = static_cast<ushort>(eventMap["repeatEvery"].toInt(&validRepeatEvery));
318 
319         if (validPeriodType && (periodType == Recurrence::rDaily) && validRepeatEvery && (repeatEvery == 1) && eventDuration > 86400) {
320             result["success"] = false;
321             result["reason"] = i18n("Daily events should not span multiple days");
322             return result;
323         }
324     }
325 
326     result["success"] = true;
327     result["reason"] = QString();
328 
329     return result;
330 
331 }
332 
upsertTodo(LocalCalendar * localCalendar,const QVariantMap & todo)333 void CalendarController::upsertTodo(LocalCalendar *localCalendar, const QVariantMap &todo)
334 {
335     Calendar::Ptr calendar = localCalendar->calendar();
336     Todo::Ptr vtodo;
337     QDateTime now = QDateTime::currentDateTime();
338     QString uid = todo["uid"].toString();
339     QString summary = todo["summary"].toString();
340     QDate startDate = todo["startDate"].toDate();
341     int startHour = todo["startHour"].value<int>();
342     int startMinute = todo["startMinute"].value<int>();
343     bool allDayFlg = todo["allDay"].toBool();
344 
345     if (uid == "") {
346         vtodo = Todo::Ptr(new Todo());
347         vtodo->setUid(summary.at(0) + now.toString("yyyyMMddhhmmsszzz"));
348     } else {
349         vtodo = calendar->todo(uid);
350         vtodo->setUid(uid);
351     }
352 
353     QDateTime startDateTime;
354     if (allDayFlg) {
355         startDateTime = startDate.startOfDay();
356     } else {
357         startDateTime = QDateTime(startDate, QTime(startHour, startMinute, 0, 0), QTimeZone::systemTimeZone());
358     }
359 
360     vtodo->setDtStart(startDateTime);
361 
362     QDate dueDate = todo["dueDate"].toDate();
363 
364     bool validDueHour {false};
365     int dueHour = todo["dueHour"].toInt(&validDueHour);
366 
367     bool validDueMinutes {false};
368     int dueMinute = todo["dueMinute"].toInt(&validDueMinutes);
369 
370     QDateTime dueDateTime = QDateTime();
371     if (dueDate.isValid() && validDueHour && validDueMinutes && !allDayFlg) {
372         dueDateTime = QDateTime(dueDate, QTime(dueHour, dueMinute, 0, 0), QTimeZone::systemTimeZone());
373     } else if (dueDate.isValid() && allDayFlg) {
374         dueDateTime = dueDate.startOfDay();
375     }
376 
377     vtodo->setDtDue(dueDateTime);
378     vtodo->setDescription(todo["description"].toString());
379     vtodo->setSummary(summary);
380     vtodo->setAllDay((startDate.isValid() || dueDate.isValid()) ? allDayFlg : false);
381     vtodo->setLocation(todo["location"].toString());
382     vtodo->setCompleted(todo["completed"].toBool());
383 
384     vtodo->clearAlarms();
385     QVariantList newAlarms = todo["alarms"].value<QVariantList>();
386     QVariantList::const_iterator itr = newAlarms.constBegin();
387     while (itr != newAlarms.constEnd()) {
388         Alarm::Ptr newAlarm = vtodo->newAlarm();
389         QHash<QString, QVariant> newAlarmHashMap = (*itr).value<QHash<QString, QVariant>>();
390         int startOffsetValue = newAlarmHashMap["startOffsetValue"].value<int>();
391         int startOffsetType = newAlarmHashMap["startOffsetType"].value<int>();
392         int actionType = newAlarmHashMap["actionType"].value<int>();
393 
394         newAlarm->setStartOffset(Duration(startOffsetValue, static_cast<Duration::Type>(startOffsetType)));
395         newAlarm->setType(static_cast<Alarm::Type>(actionType));
396         newAlarm->setEnabled(true);
397         newAlarm->setText((vtodo->summary()).isEmpty() ?  vtodo->description() : vtodo->summary());
398         ++itr;
399     }
400 
401     calendar->addTodo(vtodo);
402     bool merged = localCalendar->save();
403 
404     Q_EMIT localCalendar->todosChanged();
405 
406     qDebug() << "Todo upsert: " << merged;
407 }
408 
removeTodo(LocalCalendar * localCalendar,const QVariantMap & todo)409 void CalendarController::removeTodo(LocalCalendar *localCalendar, const QVariantMap &todo)
410 {
411     Calendar::Ptr calendar = localCalendar->calendar();
412     QString uid = todo["uid"].toString();
413     Todo::Ptr vtodo = calendar->todo(uid);
414 
415     calendar->deleteTodo(vtodo);
416     bool removed = localCalendar->save();
417 
418     Q_EMIT localCalendar->todosChanged();
419     qDebug() << "Todo deleted: " << removed;
420 }
421 
validateTodo(const QVariantMap & todo) const422 QVariantMap CalendarController::validateTodo(const QVariantMap &todo) const
423 {
424     QVariantMap result {};
425 
426     QDate startDate = todo["startDate"].toDate();
427     bool validStartHour {false};
428     int startHour = todo["startHour"].toInt(&validStartHour);
429     bool validStartMinutes {false};
430     int startMinute = todo["startMinute"].toInt(&validStartMinutes);
431     QDate dueDate = todo["dueDate"].toDate();
432     bool validDueHour {false};
433     int dueHour = todo["dueHour"].toInt(&validDueHour);
434     bool validDueMinutes {false};
435     int dueMinute = todo["dueMinute"].toInt(&validDueMinutes);
436     bool allDayFlg = todo["allDay"].toBool();
437 
438     if (startDate.isValid() && validStartHour && validStartMinutes && dueDate.isValid() && validDueHour && validDueMinutes) {
439         if (allDayFlg && (dueDate != startDate)) {
440             result["success"] = false;
441             result["reason"] = i18n("In case of all day tasks, start date and due date should be equal");
442 
443             return result;
444         }
445 
446         if (!allDayFlg && (QDateTime(startDate, QTime(startHour, startMinute, 0, 0), QTimeZone::systemTimeZone()) >  QDateTime(dueDate, QTime(dueHour, dueMinute, 0, 0), QTimeZone::systemTimeZone()))) {
447             result["success"] = false;
448             result["reason"] = i18n("Due date time should be equal to or greater than the start date time");
449             return result;
450         }
451     }
452 
453     result["success"] = true;
454     result["reason"] = QString();
455 
456     return result;
457 }
458 
fileNameFromUrl(const QUrl & sourcePath)459 QString CalendarController::fileNameFromUrl(const QUrl &sourcePath)
460 {
461     return sourcePath.fileName();
462 }
463 
exportData(const QString & calendarName)464 QVariantMap CalendarController::exportData(const QString &calendarName)
465 {
466     auto filePath = CalindoriConfig {}.calendarFile(calendarName);
467     QFile calendarFile {filePath};
468     if (!calendarFile.exists()) {
469         return {
470             { "success", false },
471             { "reason", i18n("Cannot read calendar. Export failed.") }
472         };
473     }
474 
475     Calendar::Ptr calendar {new MemoryCalendar(QTimeZone::systemTimeZoneId())};
476     FileStorage::Ptr storage {new FileStorage {calendar}};
477     storage->setFileName(filePath);
478     if (!storage->load()) {
479         return {
480             { "success", false },
481             { "reason", i18n("Cannot load calendar. Export failed.") }
482         };
483     }
484 
485     auto dirPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
486     QFile targetFile {dirPath + "/calindori_" + calendarName + ".ics"};
487     auto fileSuffix {1};
488     while (targetFile.exists()) {
489         targetFile.setFileName(dirPath + "/calindori_" + calendarName + "(" + QString::number(fileSuffix++) + ").ics");
490     }
491 
492     storage->setFileName(targetFile.fileName());
493     if (!(storage->save())) {
494         return {
495             { "success", false },
496             { "reason", i18n("Cannot save calendar file. Export failed.") }
497         };
498 
499     }
500 
501     return {
502         { "success", true },
503         { "reason", i18n("Export completed successfully") },
504         { "targetFolder", QUrl {QStringLiteral("file://") + dirPath} }
505     };
506 }
507