1 /*
2  *  kacalendar.cpp  -  KAlarm kcal library calendar and event functions
3  *  This file is part of kalarmcal library, which provides access to KAlarm
4  *  calendar data.
5  *  SPDX-FileCopyrightText: 2001-2020 David Jarvie <djarvie@kde.org>
6  *
7  *  SPDX-License-Identifier: LGPL-2.0-or-later
8  */
9 
10 #include "kacalendar.h"
11 
12 #include "kaevent.h"
13 #include "version.h"
14 
15 #include <KCalendarCore/Alarm>
16 #include <KCalendarCore/MemoryCalendar>
17 
18 #include <klocalizedstring.h>
19 #include "kalarmcal_debug.h"
20 
21 #include <QMap>
22 #include <QFile>
23 #include <QFileInfo>
24 #include <QTextStream>
25 
26 using namespace KCalendarCore;
27 
28 namespace KAlarmCal
29 {
30 
31 const QLatin1String MIME_BASE("application/x-vnd.kde.alarm");
32 const QLatin1String MIME_ACTIVE("application/x-vnd.kde.alarm.active");
33 const QLatin1String MIME_ARCHIVED("application/x-vnd.kde.alarm.archived");
34 const QLatin1String MIME_TEMPLATE("application/x-vnd.kde.alarm.template");
35 
36 static const QByteArray VERSION_PROPERTY("VERSION");     // X-KDE-KALARM-VERSION VCALENDAR property
37 
38 static bool isUTC(const QString &localFile);
39 
40 class Private
41 {
42 public:
43     static int readKAlarmVersion(const FileStorage::Ptr &, QString &subVersion, QString &versionString);
44 
45     static QByteArray mIcalProductId;
46 };
47 
48 QByteArray Private::mIcalProductId;
49 
50 //=============================================================================
51 
52 namespace KACalendar
53 {
54 
55 const QByteArray APPNAME("KALARM");
56 
setProductId(const QByteArray & progName,const QByteArray & progVersion)57 void setProductId(const QByteArray &progName, const QByteArray &progVersion)
58 {
59     Private::mIcalProductId = QByteArray("-//K Desktop Environment//NONSGML " + progName + " " + progVersion + "//EN");
60 }
61 
icalProductId()62 QByteArray icalProductId()
63 {
64     return Private::mIcalProductId.isEmpty() ? QByteArray("-//K Desktop Environment//NONSGML  //EN") : Private::mIcalProductId;
65 }
66 
67 /******************************************************************************
68 * Set the X-KDE-KALARM-VERSION property in a calendar.
69 */
setKAlarmVersion(const Calendar::Ptr & calendar)70 void setKAlarmVersion(const Calendar::Ptr &calendar)
71 {
72     calendar->setCustomProperty(APPNAME, VERSION_PROPERTY, QString::fromLatin1(KAEvent::currentCalendarVersionString()));
73 }
74 
75 /******************************************************************************
76 * Check the version of KAlarm which wrote a calendar file, and convert it in
77 * memory to the current KAlarm format if possible. The storage file is not
78 * updated. The compatibility of the calendar format is indicated by the return
79 * value.
80 */
updateVersion(const FileStorage::Ptr & fileStorage,QString & versionString)81 int updateVersion(const FileStorage::Ptr &fileStorage, QString &versionString)
82 {
83     QString subVersion;
84     const int version = Private::readKAlarmVersion(fileStorage, subVersion, versionString);
85     if (version == CurrentFormat) {
86         return CurrentFormat;    // calendar is in the current KAlarm format
87     }
88     if (version == IncompatibleFormat  ||  version > KAEvent::currentCalendarVersion()) {
89         return IncompatibleFormat;    // calendar was created by another program, or an unknown version of KAlarm
90     }
91 
92     // Calendar was created by an earlier version of KAlarm.
93     // Convert it to the current format.
94     const QString localFile = fileStorage->fileName();
95     int ver = version;
96     if (version == KAlarmCal::Version(0, 5, 7)  &&  !localFile.isEmpty()) {
97         // KAlarm version 0.5.7 - check whether times are stored in UTC, in which
98         // case it is the KDE 3.0.0 version, which needs adjustment of summer times.
99         if (isUTC(localFile)) {
100             ver = -version;
101         }
102         qCDebug(KALARMCAL_LOG) << "KAlarm version 0.5.7 (" << (ver < 0 ? "" : "non-") << "UTC)";
103     } else {
104         qCDebug(KALARMCAL_LOG) << "KAlarm version" << version;
105     }
106 
107     // Convert events to current KAlarm format for when/if the calendar is saved.
108     KAEvent::convertKCalEvents(fileStorage->calendar(), ver);
109     // Set the new calendar version.
110     setKAlarmVersion(fileStorage->calendar());
111     return version;
112 }
113 
114 } // namespace KACalendar
115 
116 /******************************************************************************
117 * Return the KAlarm version which wrote the calendar which has been loaded.
118 * The format is, for example, 000507 for 0.5.7.
119 * Reply = CurrentFormat if the calendar was created by the current version of KAlarm
120 *       = IncompatibleFormat if it was created by KAlarm pre-0.3.5, or another program
121 *       = version number if created by another KAlarm version.
122 */
readKAlarmVersion(const FileStorage::Ptr & fileStorage,QString & subVersion,QString & versionString)123 int Private::readKAlarmVersion(const FileStorage::Ptr &fileStorage, QString &subVersion, QString &versionString)
124 {
125     subVersion.clear();
126     Calendar::Ptr calendar = fileStorage->calendar();
127     versionString = calendar->customProperty(KACalendar::APPNAME, VERSION_PROPERTY);
128     qCDebug(KALARMCAL_LOG) << "File=" << fileStorage->fileName() << ", version=" << versionString;
129 
130     if (versionString.isEmpty()) {
131         // Pre-KAlarm 1.4 defined the KAlarm version number in the PRODID field.
132         // If another application has written to the file, this may not be present.
133         const QString prodid = calendar->productId();
134         if (prodid.isEmpty()) {
135             // Check whether the calendar file is empty, in which case
136             // it can be written to freely.
137             const QFileInfo fi(fileStorage->fileName());
138             if (!fi.size()) {
139                 return KACalendar::CurrentFormat;
140             }
141         }
142 
143         // Find the KAlarm identifier
144         QString progname = QStringLiteral(" KAlarm ");
145         int i = prodid.indexOf(progname, 0, Qt::CaseInsensitive);
146         if (i < 0) {
147             // Older versions used KAlarm's translated name in the product ID, which
148             // could have created problems using a calendar in different locales.
149             progname = QLatin1String(" ") + i18n("KAlarm") + QLatin1Char(' ');
150             i = prodid.indexOf(progname, 0, Qt::CaseInsensitive);
151             if (i < 0) {
152                 return KACalendar::IncompatibleFormat;    // calendar wasn't created by KAlarm
153             }
154         }
155 
156         // Extract the KAlarm version string
157         versionString = prodid.mid(i + progname.length()).trimmed();
158         i = versionString.indexOf(QLatin1Char('/'));
159         const int j = versionString.indexOf(QLatin1Char(' '));
160         if (j >= 0  &&  j < i) {
161             i = j;
162         }
163         if (i <= 0) {
164             return KACalendar::IncompatibleFormat;    // missing version string
165         }
166         versionString.truncate(i);   // 'versionString' now contains the KAlarm version string
167     }
168     if (versionString == QLatin1String(KAEvent::currentCalendarVersionString())) {
169         return KACalendar::CurrentFormat;    // the calendar is in the current KAlarm format
170     }
171     const int ver = KAlarmCal::getVersionNumber(versionString, &subVersion);
172     if (ver == KAEvent::currentCalendarVersion()) {
173         return KACalendar::CurrentFormat;    // the calendar is in the current KAlarm format
174     }
175     return KAlarmCal::getVersionNumber(versionString, &subVersion);
176 }
177 
178 /******************************************************************************
179 * Check whether the calendar file has its times stored as UTC times,
180 * indicating that it was written by the KDE 3.0.0 version of KAlarm 0.5.7.
181 * Reply = true if times are stored in UTC
182 *       = false if the calendar is a vCalendar, times are not UTC, or any error occurred.
183 */
isUTC(const QString & localFile)184 bool isUTC(const QString &localFile)
185 {
186     // Read the calendar file into a string
187     QFile file(localFile);
188     if (!file.open(QIODevice::ReadOnly)) {
189         return false;
190     }
191     QTextStream ts(&file);
192     ts.setCodec("ISO 8859-1");
193     const QByteArray text = ts.readAll().toLocal8Bit();
194     file.close();
195 
196     // Extract the CREATED property for the first VEVENT from the calendar
197     const QByteArray BEGIN_VCALENDAR("BEGIN:VCALENDAR");
198     const QByteArray BEGIN_VEVENT("BEGIN:VEVENT");
199     const QByteArray CREATED("CREATED:");
200     const QList<QByteArray> lines = text.split('\n');
201     for (int i = 0, end = lines.count();  i < end;  ++i) {
202         if (lines[i].startsWith(BEGIN_VCALENDAR)) {
203             while (++i < end) {
204                 if (lines[i].startsWith(BEGIN_VEVENT)) {
205                     while (++i < end) {
206                         if (lines[i].startsWith(CREATED)) {
207                             return lines[i].endsWith('Z');
208                         }
209                     }
210                 }
211             }
212             break;
213         }
214     }
215     return false;
216 }
217 
218 //=============================================================================
219 
220 namespace CalEvent
221 {
222 
223 // Struct to contain static strings, to allow use of Q_GLOBAL_STATIC
224 // to delete them on program termination.
225 struct StaticStrings {
StaticStringsKAlarmCal::CalEvent::StaticStrings226     StaticStrings()
227         : STATUS_PROPERTY("TYPE"),
228           ACTIVE_STATUS(QStringLiteral("ACTIVE")),
229           TEMPLATE_STATUS(QStringLiteral("TEMPLATE")),
230           ARCHIVED_STATUS(QStringLiteral("ARCHIVED")),
231           DISPLAYING_STATUS(QStringLiteral("DISPLAYING")),
232           ARCHIVED_UID(QStringLiteral("exp-")),
233           DISPLAYING_UID(QStringLiteral("disp-")),
234           OLD_ARCHIVED_UID(QStringLiteral("-exp-")),
235           OLD_TEMPLATE_UID(QStringLiteral("-tmpl-"))
236     {}
237     // Event custom properties.
238     // Note that all custom property names are prefixed with X-KDE-KALARM- in the calendar file.
239     const QByteArray STATUS_PROPERTY;    // X-KDE-KALARM-TYPE property
240     const QString ACTIVE_STATUS;
241     const QString TEMPLATE_STATUS;
242     const QString ARCHIVED_STATUS;
243     const QString DISPLAYING_STATUS;
244 
245     // Event ID identifiers
246     const QString ARCHIVED_UID;
247     const QString DISPLAYING_UID;
248 
249     // Old KAlarm format identifiers
250     const QString OLD_ARCHIVED_UID;
251     const QString OLD_TEMPLATE_UID;
252 };
Q_GLOBAL_STATIC(StaticStrings,staticStrings)253 Q_GLOBAL_STATIC(StaticStrings, staticStrings)
254 
255 /******************************************************************************
256 * Convert a unique ID to indicate that the event is in a specified calendar file.
257 * This is done by prefixing archived or displaying alarms with "exp-" or "disp-",
258 * while active alarms have no prefix.
259 * Note that previously, "-exp-" was inserted in the middle of the UID.
260 */
261 QString uid(const QString &id, Type status)
262 {
263     QString result = id;
264     Type oldType;
265     int i;
266     int len;
267     if (result.startsWith(staticStrings->ARCHIVED_UID)) {
268         oldType = ARCHIVED;
269         len = staticStrings->ARCHIVED_UID.length();
270     } else if (result.startsWith(staticStrings->DISPLAYING_UID)) {
271         oldType = DISPLAYING;
272         len = staticStrings->DISPLAYING_UID.length();
273     } else {
274         if ((i = result.indexOf(staticStrings->OLD_ARCHIVED_UID)) > 0) {
275             result.remove(i, staticStrings->OLD_ARCHIVED_UID.length());
276         }
277         oldType = ACTIVE;
278         len = 0;
279     }
280     if (status != oldType) {
281         QString part;
282         switch (status) {
283         case ARCHIVED:    part = staticStrings->ARCHIVED_UID;  break;
284         case DISPLAYING:  part = staticStrings->DISPLAYING_UID;  break;
285         case ACTIVE:
286             break;
287         case TEMPLATE:
288         case EMPTY:
289         default:
290             return result;
291         }
292         result.replace(0, len, part);
293     }
294     return result;
295 }
296 
297 /******************************************************************************
298 * Check an event to determine its type - active, archived, template or empty.
299 * The default type is active if it contains alarms and there is nothing to
300 * indicate otherwise.
301 * Note that the mere fact that all an event's alarms have passed does not make
302 * an event archived, since it may be that they have not yet been able to be
303 * triggered. They will be archived once KAlarm tries to handle them.
304 * Do not call this function for the displaying alarm calendar.
305 */
status(const Event::Ptr & event,QString * param)306 Type status(const Event::Ptr &event, QString *param)
307 {
308     // Set up a static quick lookup for type strings
309     typedef QMap<QString, Type> PropertyMap;
310     static PropertyMap properties;
311     if (properties.isEmpty()) {
312         properties[staticStrings->ACTIVE_STATUS]     = ACTIVE;
313         properties[staticStrings->TEMPLATE_STATUS]   = TEMPLATE;
314         properties[staticStrings->ARCHIVED_STATUS]   = ARCHIVED;
315         properties[staticStrings->DISPLAYING_STATUS] = DISPLAYING;
316     }
317 
318     if (param) {
319         param->clear();
320     }
321     if (!event) {
322         return EMPTY;
323     }
324     const Alarm::List alarms = event->alarms();
325     if (alarms.isEmpty()) {
326         return EMPTY;
327     }
328 
329     const QString property = event->customProperty(KACalendar::APPNAME, staticStrings->STATUS_PROPERTY);
330     if (!property.isEmpty()) {
331         // There's a X-KDE-KALARM-TYPE property.
332         // It consists of the event type, plus an optional parameter.
333         PropertyMap::ConstIterator it = properties.constFind(property);
334         if (it != properties.constEnd()) {
335             return it.value();
336         }
337         const int i = property.indexOf(QLatin1Char(';'));
338         if (i < 0) {
339             return EMPTY;
340         }
341         it = properties.constFind(property.left(i));
342         if (it == properties.constEnd()) {
343             return EMPTY;
344         }
345         if (param) {
346             *param = property.mid(i + 1);
347         }
348         return it.value();
349     }
350 
351     // The event either wasn't written by KAlarm, or was written by a pre-2.0 version.
352     // Check first for an old KAlarm format, which indicated the event type in the
353     // middle of its UID.
354     const QString uid = event->uid();
355     if (uid.indexOf(staticStrings->OLD_ARCHIVED_UID) > 0) {
356         return ARCHIVED;
357     }
358     if (uid.indexOf(staticStrings->OLD_TEMPLATE_UID) > 0) {
359         return TEMPLATE;
360     }
361 
362     // Otherwise, assume it's an active alarm
363     return ACTIVE;
364 }
365 
366 /******************************************************************************
367 * Set the event's type - active, archived, template, etc.
368 * If a parameter is supplied, it will be appended as a second parameter to the
369 * custom property.
370 */
setStatus(const Event::Ptr & event,Type status,const QString & param)371 void setStatus(const Event::Ptr &event, Type status, const QString &param)
372 {
373     if (!event) {
374         return;
375     }
376     QString text;
377     switch (status) {
378     case ACTIVE:      text = staticStrings->ACTIVE_STATUS;  break;
379     case TEMPLATE:    text = staticStrings->TEMPLATE_STATUS;  break;
380     case ARCHIVED:    text = staticStrings->ARCHIVED_STATUS;  break;
381     case DISPLAYING:  text = staticStrings->DISPLAYING_STATUS;  break;
382     default:
383         event->removeCustomProperty(KACalendar::APPNAME, staticStrings->STATUS_PROPERTY);
384         return;
385     }
386     if (!param.isEmpty()) {
387         text += QLatin1Char(';') + param;
388     }
389     event->setCustomProperty(KACalendar::APPNAME, staticStrings->STATUS_PROPERTY, text);
390 }
391 
type(const QString & mimeType)392 Type type(const QString &mimeType)
393 {
394     if (mimeType == MIME_ACTIVE) {
395         return ACTIVE;
396     }
397     if (mimeType == MIME_ARCHIVED) {
398         return ARCHIVED;
399     }
400     if (mimeType == MIME_TEMPLATE) {
401         return TEMPLATE;
402     }
403     return EMPTY;
404 }
405 
types(const QStringList & mimeTypes)406 Types types(const QStringList &mimeTypes)
407 {
408     Types types = {};
409     for (const QString &type : mimeTypes) {
410         if (type == MIME_ACTIVE) {
411             types |= ACTIVE;
412         } else if (type == MIME_ARCHIVED) {
413             types |= ARCHIVED;
414         } else if (type == MIME_TEMPLATE) {
415             types |= TEMPLATE;
416         }
417     }
418     return types;
419 }
420 
mimeType(Type type)421 QString mimeType(Type type)
422 {
423     switch (type) {
424     case ACTIVE:    return MIME_ACTIVE;
425     case ARCHIVED:  return MIME_ARCHIVED;
426     case TEMPLATE:  return MIME_TEMPLATE;
427     default:        return QString();
428     }
429 }
430 
mimeTypes(Types types)431 QStringList mimeTypes(Types types)
432 {
433     QStringList mimes;
434     for (int i = 1;  types;  i <<= 1) {
435         if (types & i) {
436             mimes += mimeType(Type(i));
437             types &= ~i;
438         }
439     }
440     return mimes;
441 }
442 
443 } // namespace CalEvent
444 
445 } // namespace KAlarmCal
446 
operator <<(QDebug debug,KAlarmCal::CalEvent::Type type)447 QDebug operator<<(QDebug debug, KAlarmCal::CalEvent::Type type)
448 {
449     const char* str;
450     switch (type) {
451     case KAlarmCal::CalEvent::ACTIVE:    str = "Active alarms";    break;
452     case KAlarmCal::CalEvent::ARCHIVED:  str = "Archived alarms";  break;
453     case KAlarmCal::CalEvent::TEMPLATE:  str = "Alarm templates";  break;
454     default:        return debug;
455     }
456     debug << str;
457     return debug;
458 }
459 
460 // vim: et sw=4:
461