1 /*
2  *  displaycalendar.cpp  -  KAlarm display calendar file access
3  *  Program:  kalarm
4  *  SPDX-FileCopyrightText: 2001-2020 David Jarvie <djarvie@kde.org>
5  *
6  *  SPDX-License-Identifier: GPL-2.0-or-later
7  */
8 
9 #include "displaycalendar.h"
10 
11 #include "mainwindow.h"
12 #include "preferences.h"
13 #include "lib/messagebox.h"
14 #include "kalarm_debug.h"
15 
16 #include <KCalendarCore/MemoryCalendar>
17 #include <KCalendarCore/ICalFormat>
18 
19 #include <KLocalizedString>
20 
21 #include <QStandardPaths>
22 #include <QDir>
23 
24 using namespace KCalendarCore;
25 using namespace KAlarmCal;
26 
27 //clazy:excludeall=non-pod-global-static
28 
29 
30 namespace
31 {
32 const QString displayCalendarName = QStringLiteral("displaying.ics");
33 }
34 
35 bool                            DisplayCalendar::mInitialised {false};
36 KAEvent::List                   DisplayCalendar::mEventList;
37 QHash<QString, KAEvent*>        DisplayCalendar::mEventMap;
38 KCalendarCore::FileStorage::Ptr DisplayCalendar::mCalendarStorage;
39 QString                         DisplayCalendar::mDisplayCalPath;
40 QString                         DisplayCalendar::mDisplayICalPath;
41 DisplayCalendar::CalType        DisplayCalendar::mCalType;
42 bool                            DisplayCalendar::mOpen {false};
43 
44 
45 /******************************************************************************
46 * Initialise the display alarm calendar.
47 * It is user-specific, and contains details of alarms which are currently being
48 * displayed to that user and which have not yet been acknowledged;
49 */
initialise()50 void DisplayCalendar::initialise()
51 {
52     QDir dir;
53     dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
54     mDisplayCalPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + displayCalendarName;
55     mDisplayICalPath = mDisplayCalPath;
56     mDisplayICalPath.replace(QStringLiteral("\\.vcs$"), QStringLiteral(".ics"));
57     mCalType = (mDisplayCalPath == mDisplayICalPath) ? LOCAL_ICAL : LOCAL_VCAL;    // is the calendar in ICal or VCal format?
58     mInitialised = true;
59 }
60 
61 /******************************************************************************
62 * Terminate access to the display calendar.
63 */
terminate()64 void DisplayCalendar::terminate()
65 {
66     close();
67     mInitialised = false;
68 }
69 
70 /******************************************************************************
71 * Open the calendar if not already open, and load it into memory.
72 */
open()73 bool DisplayCalendar::open()
74 {
75     if (mOpen)
76         return true;
77 
78     // Open the display calendar.
79     qCDebug(KALARM_LOG) << "DisplayCalendar::open:" << mDisplayCalPath;
80     if (!mCalendarStorage)
81     {
82         MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone()));
83         mCalendarStorage = FileStorage::Ptr(new FileStorage(calendar, mDisplayCalPath));
84     }
85 
86     // Check for file's existence, assuming that it does exist when uncertain,
87     // to avoid overwriting it.
88     QFileInfo fi(mDisplayCalPath);
89     if (!fi.exists()  ||  !fi.isFile()  ||  load() == 0)
90     {
91         // The calendar file doesn't yet exist, or it's zero length, so create a new one
92         if (saveCal(mDisplayICalPath))
93             load();
94     }
95 
96     if (!mOpen)
97     {
98         mCalendarStorage->calendar().clear();
99         mCalendarStorage.clear();
100     }
101     return mOpen;
102 }
103 
104 /******************************************************************************
105 * Load the calendar into memory.
106 * Reply = 1 if success
107 *       = 0 if zero-length file exists.
108 *       = -1 if failure to load calendar file
109 *       = -2 if instance uninitialised.
110 */
load()111 int DisplayCalendar::load()
112 {
113     // Load the display calendar.
114     if (!mCalendarStorage)
115         return -2;
116 
117     qCDebug(KALARM_LOG) << "DisplayCalendar::load:" << mDisplayCalPath;
118     if (!mCalendarStorage->load())
119     {
120         // Load error. Check if the file is zero length
121         QFileInfo fi(mDisplayCalPath);
122         if (fi.exists()  &&  !fi.size())
123             return 0;     // file is zero length
124 
125         qCCritical(KALARM_LOG) << "DisplayCalendar::load: Error loading calendar file '" << mDisplayCalPath <<"'";
126         KAMessageBox::error(MainWindow::mainMainWindow(),
127                             xi18nc("@info", "<para>Error loading calendar:</para><para><filename>%1</filename></para><para>Please fix or delete the file.</para>", mDisplayCalPath));
128         // load() could have partially populated the calendar, so clear it out
129         mCalendarStorage->calendar()->close();
130         mCalendarStorage->calendar().clear();
131         mCalendarStorage.clear();
132         mOpen = false;
133         return -1;
134     }
135     QString versionString;
136     KACalendar::updateVersion(mCalendarStorage, versionString);   // convert events to current KAlarm format for when calendar is saved
137     updateKAEvents();
138 
139     mOpen = true;
140     return 1;
141 }
142 
143 /******************************************************************************
144 * Save the calendar.
145 */
save()146 bool DisplayCalendar::save()
147 {
148     return saveCal();
149 }
150 
151 /******************************************************************************
152 * Save the calendar from memory to file.
153 * If a filename is specified, create a new calendar file.
154 */
saveCal(const QString & newFile)155 bool DisplayCalendar::saveCal(const QString& newFile)
156 {
157     if (!mCalendarStorage)
158         return false;
159     if (!mOpen  &&  newFile.isEmpty())
160         return false;
161 
162     qCDebug(KALARM_LOG) << "DisplayCalendar::saveCal:" << "\"" << newFile;
163     QString saveFilename = newFile.isEmpty() ? mDisplayCalPath : newFile;
164     if (mCalType == LOCAL_VCAL  &&  newFile.isNull())
165         saveFilename = mDisplayICalPath;
166     mCalendarStorage->setFileName(saveFilename);
167     mCalendarStorage->setSaveFormat(new ICalFormat);
168     if (!mCalendarStorage->save())
169     {
170         qCCritical(KALARM_LOG) << "DisplayCalendar::saveCal: Saving" << saveFilename << "failed.";
171         KAMessageBox::error(MainWindow::mainMainWindow(),
172                             xi18nc("@info", "Failed to save calendar to <filename>%1</filename>", mDisplayICalPath));
173         return false;
174     }
175 
176     if (mCalType == LOCAL_VCAL)
177     {
178         // The file was in vCalendar format, but has now been saved in iCalendar format.
179         mDisplayCalPath = mDisplayICalPath;
180         mCalType = LOCAL_ICAL;
181     }
182     return true;
183 }
184 
185 /******************************************************************************
186 * Close display calendar file at program exit.
187 */
close()188 void DisplayCalendar::close()
189 {
190     if (mCalendarStorage)
191     {
192         mCalendarStorage->calendar()->close();
193         mCalendarStorage->calendar().clear();
194         mCalendarStorage.clear();
195     }
196 
197     // Flag as closed now to prevent removeKAEvents() doing silly things
198     // when it's called again
199     mOpen = false;
200 
201     // Events list should be empty, but just in case...
202     for (KAEvent* event : std::as_const(mEventList))
203     {
204         mEventMap.remove(event->id());
205         delete event;
206     }
207     mEventList.clear();
208 }
209 
210 /******************************************************************************
211 * Create a KAEvent instance corresponding to each KCalendarCore::Event in the
212 * display calendar, and store them in the event map in place of the old set.
213 * Called after the display calendar has completed loading.
214 */
updateKAEvents()215 void DisplayCalendar::updateKAEvents()
216 {
217     qCDebug(KALARM_LOG) << "DisplayCalendar::updateKAEvents";
218     for (KAEvent* event : std::as_const(mEventList))
219     {
220         mEventMap.remove(event->id());
221         delete event;
222     }
223     mEventList.clear();
224     Calendar::Ptr cal = mCalendarStorage->calendar();
225     if (!cal)
226         return;
227 
228     const KCalendarCore::Event::List kcalevents = cal->rawEvents();
229     for (const KCalendarCore::Event::Ptr& kcalevent : kcalevents)
230     {
231         if (kcalevent->alarms().isEmpty())
232             continue;    // ignore events without alarms
233 
234         auto event = new KAEvent(kcalevent);
235         if (!event->isValid())
236         {
237             qCWarning(KALARM_LOG) << "DisplayCalendar::updateKAEvents: Ignoring unusable event" << kcalevent->uid();
238             delete event;
239             continue;    // ignore events without usable alarms
240         }
241         mEventList += event;
242         mEventMap[kcalevent->uid()] = event;
243     }
244 }
245 
246 /******************************************************************************
247 * Add the specified event to the calendar.
248 * Reply = true if 'evnt' was written to the calendar. 'evnt' is updated.
249 *       = false if an error occurred, in which case 'evnt' is unchanged.
250 */
addEvent(KAEvent & evnt)251 bool DisplayCalendar::addEvent(KAEvent& evnt)
252 {
253     if (!mOpen)
254         return false;
255     qCDebug(KALARM_LOG) << "DisplayCalendar::addEvent:" << evnt.id();
256     // Check that the event type is valid for the calendar
257     const CalEvent::Type type = evnt.category();
258     if (type != CalEvent::DISPLAYING)
259         return false;
260 
261     KCalendarCore::Event::Ptr kcalEvent(new KCalendarCore::Event);
262     auto event = new KAEvent(evnt);
263     QString id = event->id();
264     if (id.isEmpty())
265         id = kcalEvent->uid();
266     id = CalEvent::uid(id, type);   // include the alarm type tag in the ID
267     kcalEvent->setUid(id);
268     event->setEventId(id);
269     event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE);
270 
271     bool ok = false;
272     bool remove = false;
273     if (!mEventMap.contains(event->id()))
274     {
275         mEventList += event;
276         mEventMap[event->id()] = event;
277         ok = mCalendarStorage->calendar()->addEvent(kcalEvent);
278         remove = !ok;
279     }
280     if (!ok)
281     {
282         if (remove)
283         {
284             // Adding to mCalendar failed, so undo DisplayCalendar::addEvent()
285             mEventMap.remove(event->id());
286             int i = mEventList.indexOf(event);
287             if (i >= 0)
288                 mEventList.remove(i);
289         }
290         delete event;
291         return false;
292     }
293     evnt = *event;
294     if (remove)
295         delete event;
296     return true;
297 }
298 
299 /******************************************************************************
300 * Delete the specified event from the calendar, if it exists.
301 * The calendar is then optionally saved.
302 */
deleteEvent(const QString & eventID,bool saveit)303 bool DisplayCalendar::deleteEvent(const QString& eventID, bool saveit)
304 {
305     if (mOpen)
306     {
307         KCalendarCore::Event::Ptr kcalEvent;
308         if (mCalendarStorage)
309             kcalEvent = mCalendarStorage->calendar()->event(eventID);   // display calendar
310 
311         // Make a copy of the ID QString, since the supplied reference might be
312         // destructed when the event is deleted below.
313         const QString id = eventID;
314 
315         auto it = mEventMap.find(id);
316         if (it != mEventMap.end())
317         {
318             KAEvent* ev = it.value();
319             mEventMap.erase(it);
320             int i = mEventList.indexOf(ev);
321             if (i >= 0)
322                 mEventList.remove(i);
323             delete ev;
324         }
325 
326         CalEvent::Type status = CalEvent::EMPTY;
327         if (kcalEvent)
328         {
329             status = CalEvent::status(kcalEvent);
330             mCalendarStorage->calendar()->deleteEvent(kcalEvent);
331         }
332 
333         if (status != CalEvent::EMPTY)
334         {
335             if (saveit)
336                 return saveCal();
337             return true;
338         }
339     }
340     return false;
341 }
342 
343 /******************************************************************************
344 * Return the event with the specified ID.
345 * This method is for the display calendar only.
346 */
kcalEvent(const QString & uniqueID)347 KCalendarCore::Event::Ptr DisplayCalendar::kcalEvent(const QString& uniqueID)
348 {
349     if (!mCalendarStorage)
350         return KCalendarCore::Event::Ptr();
351     return mCalendarStorage->calendar()->event(uniqueID);
352 }
353 
354 /******************************************************************************
355 * Return all events in the calendar which contain usable alarms.
356 * This method is for the display calendar only.
357 * Optionally the event type can be filtered, using an OR of event types.
358 */
kcalEvents(CalEvent::Type type)359 KCalendarCore::Event::List DisplayCalendar::kcalEvents(CalEvent::Type type)
360 {
361     KCalendarCore::Event::List list;
362     if (!mCalendarStorage)
363         return list;
364     list = mCalendarStorage->calendar()->rawEvents();
365     for (int i = 0;  i < list.count();  )
366     {
367         KCalendarCore::Event::Ptr event = list.at(i);
368         if (event->alarms().isEmpty()
369         ||  (type != CalEvent::EMPTY  &&  !(type & CalEvent::status(event)))
370         ||  !KAEvent(event).isValid())
371             list.remove(i);
372         else
373             ++i;
374     }
375     return list;
376 }
377 
378 /******************************************************************************
379 * Called when the user changes the start-of-day time.
380 * Adjust the start times of all date-only alarms' recurrences.
381 */
adjustStartOfDay()382 void DisplayCalendar::adjustStartOfDay()
383 {
384     if (!isValid())
385         return;
386     KAEvent::adjustStartOfDay(mEventList);
387 }
388 
389 // vim: et sw=4:
390