1 /*
2   This file is part of the kcalcore library.
3 
4   SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
5   SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
6 
7   SPDX-License-Identifier: LGPL-2.0-or-later
8 */
9 /**
10   @file
11   This file is part of the API for handling calendar data and
12   defines the Incidence class.
13 
14   @brief
15   Provides the class common to non-FreeBusy (Events, To-dos, Journals)
16   calendar components known as incidences.
17 
18   @author Cornelius Schumacher \<schumacher@kde.org\>
19   @author Reinhold Kainhofer \<reinhold@kainhofer.com\>
20 */
21 
22 #include "incidence.h"
23 #include "incidence_p.h"
24 #include "calformat.h"
25 #include "kcalendarcore_debug.h"
26 #include "utils_p.h"
27 
28 #include <math.h>
29 
30 #include <QStringList>
31 #include <QTextDocument> // for .toHtmlEscaped() and Qt::mightBeRichText()
32 #include <QTime>
33 #include <QTimeZone>
34 
35 using namespace KCalendarCore;
36 
37 IncidencePrivate::IncidencePrivate() = default;
38 
IncidencePrivate(const IncidencePrivate & p)39 IncidencePrivate::IncidencePrivate(const IncidencePrivate &p)
40     : mCreated(p.mCreated)
41     , mDescription(p.mDescription)
42     , mSummary(p.mSummary)
43     , mLocation(p.mLocation)
44     , mCategories(p.mCategories)
45     , mResources(p.mResources)
46     , mStatusString(p.mStatusString)
47     , mSchedulingID(p.mSchedulingID)
48     , mRelatedToUid(p.mRelatedToUid)
49     , mRecurrenceId(p.mRecurrenceId)
50     , mConferences(p.mConferences)
51     , mGeoLatitude(p.mGeoLatitude)
52     , mGeoLongitude(p.mGeoLongitude)
53     , mRecurrence(nullptr)
54     , mRevision(p.mRevision)
55     , mPriority(p.mPriority)
56     , mStatus(p.mStatus)
57     , mSecrecy(p.mSecrecy)
58     , mColor(p.mColor)
59     , mDescriptionIsRich(p.mDescriptionIsRich)
60     , mSummaryIsRich(p.mSummaryIsRich)
61     , mLocationIsRich(p.mLocationIsRich)
62     , mThisAndFuture(p.mThisAndFuture)
63     , mLocalOnly(false)
64 {
65 }
66 
clear()67 void IncidencePrivate::clear()
68 {
69     mAlarms.clear();
70     mAttachments.clear();
71     delete mRecurrence;
72     mRecurrence = nullptr;
73 }
74 
init(Incidence * q,const IncidencePrivate & other)75 void IncidencePrivate::init(Incidence *q, const IncidencePrivate &other)
76 {
77     mRevision = other.mRevision;
78     mCreated = other.mCreated;
79     mDescription = other.mDescription;
80     mDescriptionIsRich = other.mDescriptionIsRich;
81     mSummary = other.mSummary;
82     mSummaryIsRich = other.mSummaryIsRich;
83     mCategories = other.mCategories;
84     mRelatedToUid = other.mRelatedToUid;
85     mResources = other.mResources;
86     mStatusString = other.mStatusString;
87     mStatus = other.mStatus;
88     mSecrecy = other.mSecrecy;
89     mPriority = other.mPriority;
90     mLocation = other.mLocation;
91     mLocationIsRich = other.mLocationIsRich;
92     mGeoLatitude = other.mGeoLatitude;
93     mGeoLongitude = other.mGeoLongitude;
94     mRecurrenceId = other.mRecurrenceId;
95     mConferences = other.mConferences;
96     mThisAndFuture = other.mThisAndFuture;
97     mLocalOnly = other.mLocalOnly;
98     mColor = other.mColor;
99 
100     // Alarms and Attachments are stored in ListBase<...>, which is a QValueList<...*>.
101     // We need to really duplicate the objects stored therein, otherwise deleting
102     // i will also delete all attachments from this object (setAutoDelete...)
103     mAlarms.reserve(other.mAlarms.count());
104     for (const Alarm::Ptr &alarm : qAsConst(other.mAlarms)) {
105         Alarm::Ptr b(new Alarm(*alarm.data()));
106         b->setParent(q);
107         mAlarms.append(b);
108     }
109 
110     mAttachments = other.mAttachments;
111     if (other.mRecurrence) {
112         mRecurrence = new Recurrence(*(other.mRecurrence));
113         mRecurrence->addObserver(q);
114     } else {
115         mRecurrence = nullptr;
116     }
117 }
118 //@endcond
119 
Incidence()120 Incidence::Incidence()
121     : IncidenceBase()
122     , d(new KCalendarCore::IncidencePrivate)
123 {
124     recreate();
125     resetDirtyFields();
126 }
127 
Incidence(const Incidence & i)128 Incidence::Incidence(const Incidence &i)
129     : IncidenceBase(i)
130     , Recurrence::RecurrenceObserver()
131     , d(new KCalendarCore::IncidencePrivate(*i.d))
132 {
133     d->init(this, *i.d);
134     resetDirtyFields();
135 }
136 
~Incidence()137 Incidence::~Incidence()
138 {
139     // Alarm has a raw incidence pointer, so we must set it to 0
140     // so Alarm doesn't use it after Incidence is destroyed
141     for (const Alarm::Ptr &alarm : qAsConst(d->mAlarms)) {
142         alarm->setParent(nullptr);
143     }
144     delete d->mRecurrence;
145     delete d;
146 }
147 
148 //@cond PRIVATE
149 // A string comparison that considers that null and empty are the same
stringCompare(const QString & s1,const QString & s2)150 static bool stringCompare(const QString &s1, const QString &s2)
151 {
152     return (s1.isEmpty() && s2.isEmpty()) || (s1 == s2);
153 }
154 
155 //@endcond
assign(const IncidenceBase & other)156 IncidenceBase &Incidence::assign(const IncidenceBase &other)
157 {
158     if (&other != this) {
159         d->clear();
160         // TODO: should relations be cleared out, as in destructor???
161         IncidenceBase::assign(other);
162         const Incidence *i = static_cast<const Incidence *>(&other);
163         d->init(this, *(i->d));
164     }
165 
166     return *this;
167 }
168 
equals(const IncidenceBase & incidence) const169 bool Incidence::equals(const IncidenceBase &incidence) const
170 {
171     if (!IncidenceBase::equals(incidence)) {
172         return false;
173     }
174 
175     // If they weren't the same type IncidenceBase::equals would had returned false already
176     const Incidence *i2 = static_cast<const Incidence *>(&incidence);
177 
178     const Alarm::List alarmList = alarms();
179     const Alarm::List otherAlarmsList = i2->alarms();
180     if (alarmList.count() != otherAlarmsList.count()) {
181         return false;
182     }
183 
184     auto matchFunc = [](const Alarm::Ptr &a, const Alarm::Ptr &b) {
185         return *a == *b;
186     };
187 
188     const auto [it1, it2] = std::mismatch(alarmList.cbegin(), alarmList.cend(), otherAlarmsList.cbegin(), otherAlarmsList.cend(), matchFunc);
189     // Checking the iterator from one list only, since both lists are the same size
190     if (it1 != alarmList.cend()) {
191         return false;
192     }
193 
194     const Attachment::List attachmentList = attachments();
195     const Attachment::List otherAttachmentList = i2->attachments();
196     if (attachmentList.count() != otherAttachmentList.count()) {
197         return false;
198     }
199 
200     const auto [at1, at2] = std::mismatch(attachmentList.cbegin(), attachmentList.cend(), otherAttachmentList.cbegin(), otherAttachmentList.cend());
201 
202     // Checking the iterator from one list only, since both lists are the same size
203     if (at1 != attachmentList.cend()) {
204         return false;
205     }
206 
207     bool recurrenceEqual = (d->mRecurrence == nullptr && i2->d->mRecurrence == nullptr);
208     if (!recurrenceEqual) {
209         recurrence(); // create if doesn't exist
210         i2->recurrence(); // create if doesn't exist
211         recurrenceEqual = d->mRecurrence != nullptr && i2->d->mRecurrence != nullptr && *d->mRecurrence == *i2->d->mRecurrence;
212     }
213 
214     if (!qFuzzyCompare(d->mGeoLatitude, i2->d->mGeoLatitude) || !qFuzzyCompare(d->mGeoLongitude, i2->d->mGeoLongitude)) {
215         return false;
216     }
217     // clang-format off
218     return
219         recurrenceEqual
220         && created() == i2->created()
221         && stringCompare(description(), i2->description())
222         && descriptionIsRich() == i2->descriptionIsRich()
223         && stringCompare(summary(), i2->summary())
224         && summaryIsRich() == i2->summaryIsRich()
225         && categories() == i2->categories()
226         && stringCompare(relatedTo(), i2->relatedTo())
227         && resources() == i2->resources()
228         && d->mStatus == i2->d->mStatus
229         && (d->mStatus == StatusNone || stringCompare(d->mStatusString, i2->d->mStatusString))
230         && secrecy() == i2->secrecy()
231         && priority() == i2->priority()
232         && stringCompare(location(), i2->location())
233         && locationIsRich() == i2->locationIsRich()
234         && stringCompare(color(), i2->color())
235         && stringCompare(schedulingID(), i2->schedulingID())
236         && recurrenceId() == i2->recurrenceId()
237         && conferences() == i2->conferences()
238         && thisAndFuture() == i2->thisAndFuture();
239     // clang-format on
240 }
241 
instanceIdentifier() const242 QString Incidence::instanceIdentifier() const
243 {
244     if (hasRecurrenceId()) {
245         return uid() + recurrenceId().toString(Qt::ISODate);
246     }
247     return uid();
248 }
249 
recreate()250 void Incidence::recreate()
251 {
252     const QDateTime nowUTC = QDateTime::currentDateTimeUtc();
253     setCreated(nowUTC);
254 
255     setSchedulingID(QString(), CalFormat::createUniqueId());
256     setRevision(0);
257     setLastModified(nowUTC); // NOLINT false clang-analyzer-optin.cplusplus.VirtualCall
258 }
259 
setLastModified(const QDateTime & lm)260 void Incidence::setLastModified(const QDateTime &lm)
261 {
262     if (!d->mLocalOnly) {
263         IncidenceBase::setLastModified(lm);
264     }
265 }
266 
setReadOnly(bool readOnly)267 void Incidence::setReadOnly(bool readOnly)
268 {
269     IncidenceBase::setReadOnly(readOnly);
270     if (d->mRecurrence) {
271         d->mRecurrence->setRecurReadOnly(readOnly);
272     }
273 }
274 
setLocalOnly(bool localOnly)275 void Incidence::setLocalOnly(bool localOnly)
276 {
277     if (mReadOnly) {
278         return;
279     }
280     d->mLocalOnly = localOnly;
281 }
282 
localOnly() const283 bool Incidence::localOnly() const
284 {
285     return d->mLocalOnly;
286 }
287 
setAllDay(bool allDay)288 void Incidence::setAllDay(bool allDay)
289 {
290     if (mReadOnly) {
291         return;
292     }
293     if (d->mRecurrence) {
294         d->mRecurrence->setAllDay(allDay);
295     }
296     IncidenceBase::setAllDay(allDay);
297 }
298 
setCreated(const QDateTime & created)299 void Incidence::setCreated(const QDateTime &created)
300 {
301     if (mReadOnly || d->mLocalOnly) {
302         return;
303     }
304 
305     update();
306     d->mCreated = created.toUTC();
307     const auto ct = d->mCreated.time();
308     // Remove milliseconds
309     d->mCreated.setTime(QTime(ct.hour(), ct.minute(), ct.second()));
310     setFieldDirty(FieldCreated);
311     updated();
312 }
313 
created() const314 QDateTime Incidence::created() const
315 {
316     return d->mCreated;
317 }
318 
setRevision(int rev)319 void Incidence::setRevision(int rev)
320 {
321     if (mReadOnly || d->mLocalOnly) {
322         return;
323     }
324 
325     update();
326     d->mRevision = rev;
327     setFieldDirty(FieldRevision);
328     updated();
329 }
330 
revision() const331 int Incidence::revision() const
332 {
333     return d->mRevision;
334 }
335 
setDtStart(const QDateTime & dt)336 void Incidence::setDtStart(const QDateTime &dt)
337 {
338     IncidenceBase::setDtStart(dt);
339     if (d->mRecurrence && dirtyFields().contains(FieldDtStart)) {
340         d->mRecurrence->setStartDateTime(dt, allDay());
341     }
342 }
343 
shiftTimes(const QTimeZone & oldZone,const QTimeZone & newZone)344 void Incidence::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone)
345 {
346     IncidenceBase::shiftTimes(oldZone, newZone);
347     if (d->mRecurrence) {
348         d->mRecurrence->shiftTimes(oldZone, newZone);
349     }
350     if (d->mAlarms.count() > 0) {
351         update();
352         for (auto alarm : d->mAlarms) {
353             alarm->shiftTimes(oldZone, newZone);
354         }
355         setFieldDirty(FieldAlarms);
356         updated();
357     }
358 }
359 
setDescription(const QString & description,bool isRich)360 void Incidence::setDescription(const QString &description, bool isRich)
361 {
362     if (mReadOnly) {
363         return;
364     }
365     update();
366     d->mDescription = description;
367     d->mDescriptionIsRich = isRich;
368     setFieldDirty(FieldDescription);
369     updated();
370 }
371 
setDescription(const QString & description)372 void Incidence::setDescription(const QString &description)
373 {
374     setDescription(description, Qt::mightBeRichText(description));
375 }
376 
description() const377 QString Incidence::description() const
378 {
379     return d->mDescription;
380 }
381 
richDescription() const382 QString Incidence::richDescription() const
383 {
384     if (descriptionIsRich()) {
385         return d->mDescription;
386     } else {
387         return d->mDescription.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>"));
388     }
389 }
390 
descriptionIsRich() const391 bool Incidence::descriptionIsRich() const
392 {
393     return d->mDescriptionIsRich;
394 }
395 
setSummary(const QString & summary,bool isRich)396 void Incidence::setSummary(const QString &summary, bool isRich)
397 {
398     if (mReadOnly) {
399         return;
400     }
401     if (d->mSummary != summary || d->mSummaryIsRich != isRich) {
402         update();
403         d->mSummary = summary;
404         d->mSummaryIsRich = isRich;
405         setFieldDirty(FieldSummary);
406         updated();
407     }
408 }
409 
setSummary(const QString & summary)410 void Incidence::setSummary(const QString &summary)
411 {
412     setSummary(summary, Qt::mightBeRichText(summary));
413 }
414 
summary() const415 QString Incidence::summary() const
416 {
417     return d->mSummary;
418 }
419 
richSummary() const420 QString Incidence::richSummary() const
421 {
422     if (summaryIsRich()) {
423         return d->mSummary;
424     } else {
425         return d->mSummary.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>"));
426     }
427 }
428 
summaryIsRich() const429 bool Incidence::summaryIsRich() const
430 {
431     return d->mSummaryIsRich;
432 }
433 
setCategories(const QStringList & categories)434 void Incidence::setCategories(const QStringList &categories)
435 {
436     if (mReadOnly) {
437         return;
438     }
439 
440     update();
441     d->mCategories = categories;
442     setFieldDirty(FieldCategories);
443     updated();
444 }
445 
setCategories(const QString & catStr)446 void Incidence::setCategories(const QString &catStr)
447 {
448     if (mReadOnly) {
449         return;
450     }
451     update();
452     setFieldDirty(FieldCategories);
453 
454     d->mCategories.clear();
455 
456     if (catStr.isEmpty()) {
457         updated();
458         return;
459     }
460 
461     d->mCategories = catStr.split(QLatin1Char(','));
462 
463     for (auto &category : d->mCategories) {
464         category = category.trimmed();
465     }
466 
467     updated();
468 }
469 
categories() const470 QStringList Incidence::categories() const
471 {
472     return d->mCategories;
473 }
474 
categoriesStr() const475 QString Incidence::categoriesStr() const
476 {
477     return d->mCategories.join(QLatin1Char(','));
478 }
479 
setRelatedTo(const QString & relatedToUid,RelType relType)480 void Incidence::setRelatedTo(const QString &relatedToUid, RelType relType)
481 {
482     // TODO: RFC says that an incidence can have more than one related-to field
483     // even for the same relType.
484 
485     if (d->mRelatedToUid[relType] != relatedToUid) {
486         update();
487         d->mRelatedToUid[relType] = relatedToUid;
488         setFieldDirty(FieldRelatedTo);
489         updated();
490     }
491 }
492 
relatedTo(RelType relType) const493 QString Incidence::relatedTo(RelType relType) const
494 {
495     return d->mRelatedToUid.value(relType);
496 }
497 
setColor(const QString & colorName)498 void Incidence::setColor(const QString &colorName)
499 {
500     if (mReadOnly) {
501         return;
502     }
503     if (!stringCompare(d->mColor, colorName)) {
504         update();
505         d->mColor = colorName;
506         setFieldDirty(FieldColor);
507         updated();
508     }
509 }
510 
color() const511 QString Incidence::color() const
512 {
513     return d->mColor;
514 }
515 
516 // %%%%%%%%%%%%  Recurrence-related methods %%%%%%%%%%%%%%%%%%%%
517 
recurrence() const518 Recurrence *Incidence::recurrence() const
519 {
520     if (!d->mRecurrence) {
521         d->mRecurrence = new Recurrence();
522         d->mRecurrence->setStartDateTime(dateTime(RoleRecurrenceStart), allDay());
523         d->mRecurrence->setAllDay(allDay());
524         d->mRecurrence->setRecurReadOnly(mReadOnly);
525         d->mRecurrence->addObserver(const_cast<KCalendarCore::Incidence *>(this));
526     }
527 
528     return d->mRecurrence;
529 }
530 
clearRecurrence()531 void Incidence::clearRecurrence()
532 {
533     delete d->mRecurrence;
534     d->mRecurrence = nullptr;
535 }
536 
recurrenceType() const537 ushort Incidence::recurrenceType() const
538 {
539     if (d->mRecurrence) {
540         return d->mRecurrence->recurrenceType();
541     } else {
542         return Recurrence::rNone;
543     }
544 }
545 
recurs() const546 bool Incidence::recurs() const
547 {
548     if (d->mRecurrence) {
549         return d->mRecurrence->recurs();
550     } else {
551         return false;
552     }
553 }
554 
recursOn(const QDate & date,const QTimeZone & timeZone) const555 bool Incidence::recursOn(const QDate &date, const QTimeZone &timeZone) const
556 {
557     return d->mRecurrence && d->mRecurrence->recursOn(date, timeZone);
558 }
559 
recursAt(const QDateTime & qdt) const560 bool Incidence::recursAt(const QDateTime &qdt) const
561 {
562     return d->mRecurrence && d->mRecurrence->recursAt(qdt);
563 }
564 
startDateTimesForDate(const QDate & date,const QTimeZone & timeZone) const565 QList<QDateTime> Incidence::startDateTimesForDate(const QDate &date, const QTimeZone &timeZone) const
566 {
567     QDateTime start = dtStart();
568     QDateTime end = dateTime(RoleEndRecurrenceBase);
569 
570     QList<QDateTime> result;
571 
572     // TODO_Recurrence: Also work if only due date is given...
573     if (!start.isValid() && !end.isValid()) {
574         return result;
575     }
576 
577     // if the incidence doesn't recur,
578     QDateTime kdate(date, {}, timeZone);
579     if (!recurs()) {
580         if (start.date() <= date && end.date() >= date) {
581             result << start;
582         }
583         return result;
584     }
585 
586     qint64 days = start.daysTo(end);
587     // Account for possible recurrences going over midnight, while the original event doesn't
588     QDate tmpday(date.addDays(-days - 1));
589     QDateTime tmp;
590     while (tmpday <= date) {
591         if (recurrence()->recursOn(tmpday, timeZone)) {
592             const QList<QTime> times = recurrence()->recurTimesOn(tmpday, timeZone);
593             for (const QTime &time : times) {
594                 tmp = QDateTime(tmpday, time, start.timeZone());
595                 if (endDateForStart(tmp) >= kdate) {
596                     result << tmp;
597                 }
598             }
599         }
600         tmpday = tmpday.addDays(1);
601     }
602     return result;
603 }
604 
startDateTimesForDateTime(const QDateTime & datetime) const605 QList<QDateTime> Incidence::startDateTimesForDateTime(const QDateTime &datetime) const
606 {
607     QDateTime start = dtStart();
608     QDateTime end = dateTime(RoleEndRecurrenceBase);
609 
610     QList<QDateTime> result;
611 
612     // TODO_Recurrence: Also work if only due date is given...
613     if (!start.isValid() && !end.isValid()) {
614         return result;
615     }
616 
617     // if the incidence doesn't recur,
618     if (!recurs()) {
619         if (!(start > datetime || end < datetime)) {
620             result << start;
621         }
622         return result;
623     }
624 
625     qint64 days = start.daysTo(end);
626     // Account for possible recurrences going over midnight, while the original event doesn't
627     QDate tmpday(datetime.date().addDays(-days - 1));
628     QDateTime tmp;
629     while (tmpday <= datetime.date()) {
630         if (recurrence()->recursOn(tmpday, datetime.timeZone())) {
631             // Get the times during the day (in start date's time zone) when recurrences happen
632             const QList<QTime> times = recurrence()->recurTimesOn(tmpday, start.timeZone());
633             for (const QTime &time : times) {
634                 tmp = QDateTime(tmpday, time, start.timeZone());
635                 if (!(tmp > datetime || endDateForStart(tmp) < datetime)) {
636                     result << tmp;
637                 }
638             }
639         }
640         tmpday = tmpday.addDays(1);
641     }
642     return result;
643 }
644 
endDateForStart(const QDateTime & startDt) const645 QDateTime Incidence::endDateForStart(const QDateTime &startDt) const
646 {
647     QDateTime start = dtStart();
648     QDateTime end = dateTime(RoleEndRecurrenceBase);
649     if (!end.isValid()) {
650         return start;
651     }
652     if (!start.isValid()) {
653         return end;
654     }
655 
656     return startDt.addSecs(start.secsTo(end));
657 }
658 
addAttachment(const Attachment & attachment)659 void Incidence::addAttachment(const Attachment &attachment)
660 {
661     if (mReadOnly || attachment.isEmpty()) {
662         return;
663     }
664 
665     update();
666     d->mAttachments.append(attachment);
667     setFieldDirty(FieldAttachment);
668     updated();
669 }
670 
deleteAttachments(const QString & mime)671 void Incidence::deleteAttachments(const QString &mime)
672 {
673     auto it = std::remove_if(d->mAttachments.begin(), d->mAttachments.end(), [&mime](const Attachment &a) {
674         return a.mimeType() == mime;
675     });
676     if (it != d->mAttachments.end()) {
677         update();
678         d->mAttachments.erase(it, d->mAttachments.end());
679         setFieldDirty(FieldAttachment);
680         updated();
681     }
682 }
683 
attachments() const684 Attachment::List Incidence::attachments() const
685 {
686     return d->mAttachments;
687 }
688 
attachments(const QString & mime) const689 Attachment::List Incidence::attachments(const QString &mime) const
690 {
691     Attachment::List attachments;
692     for (const Attachment &attachment : qAsConst(d->mAttachments)) {
693         if (attachment.mimeType() == mime) {
694             attachments.append(attachment);
695         }
696     }
697     return attachments;
698 }
699 
clearAttachments()700 void Incidence::clearAttachments()
701 {
702     update();
703     setFieldDirty(FieldAttachment);
704     d->mAttachments.clear();
705     updated();
706 }
707 
setResources(const QStringList & resources)708 void Incidence::setResources(const QStringList &resources)
709 {
710     if (mReadOnly) {
711         return;
712     }
713 
714     update();
715     d->mResources = resources;
716     setFieldDirty(FieldResources);
717     updated();
718 }
719 
resources() const720 QStringList Incidence::resources() const
721 {
722     return d->mResources;
723 }
724 
setPriority(int priority)725 void Incidence::setPriority(int priority)
726 {
727     if (mReadOnly) {
728         return;
729     }
730 
731     if (priority < 0 || priority > 9) {
732         qCWarning(KCALCORE_LOG)  << "Ignoring invalid priority" << priority;
733         return;
734     }
735 
736     update();
737     d->mPriority = priority;
738     setFieldDirty(FieldPriority);
739     updated();
740 }
741 
priority() const742 int Incidence::priority() const
743 {
744     return d->mPriority;
745 }
746 
setStatus(Incidence::Status status)747 void Incidence::setStatus(Incidence::Status status)
748 {
749     if (mReadOnly || status == StatusX) {
750         return;
751     }
752 
753     update();
754     d->mStatus = status;
755     d->mStatusString.clear();
756     setFieldDirty(FieldStatus);
757     updated();
758 }
759 
setCustomStatus(const QString & status)760 void Incidence::setCustomStatus(const QString &status)
761 {
762     if (mReadOnly) {
763         return;
764     }
765 
766     update();
767     d->mStatus = status.isEmpty() ? StatusNone : StatusX;
768     d->mStatusString = status;
769     setFieldDirty(FieldStatus);
770     updated();
771 }
772 
status() const773 Incidence::Status Incidence::status() const
774 {
775     return d->mStatus;
776 }
777 
customStatus() const778 QString Incidence::customStatus() const
779 {
780     if (d->mStatus == StatusX) {
781         return d->mStatusString;
782     } else {
783         return QString();
784     }
785 }
786 
setSecrecy(Incidence::Secrecy secrecy)787 void Incidence::setSecrecy(Incidence::Secrecy secrecy)
788 {
789     if (mReadOnly) {
790         return;
791     }
792 
793     update();
794     d->mSecrecy = secrecy;
795     setFieldDirty(FieldSecrecy);
796     updated();
797 }
798 
secrecy() const799 Incidence::Secrecy Incidence::secrecy() const
800 {
801     return d->mSecrecy;
802 }
803 
alarms() const804 Alarm::List Incidence::alarms() const
805 {
806     return d->mAlarms;
807 }
808 
newAlarm()809 Alarm::Ptr Incidence::newAlarm()
810 {
811     Alarm::Ptr alarm(new Alarm(this));
812     addAlarm(alarm);
813     return alarm;
814 }
815 
addAlarm(const Alarm::Ptr & alarm)816 void Incidence::addAlarm(const Alarm::Ptr &alarm)
817 {
818     update();
819     d->mAlarms.append(alarm);
820     setFieldDirty(FieldAlarms);
821     updated();
822 }
823 
removeAlarm(const Alarm::Ptr & alarm)824 void Incidence::removeAlarm(const Alarm::Ptr &alarm)
825 {
826     const int index = d->mAlarms.indexOf(alarm);
827     if (index > -1) {
828         update();
829         d->mAlarms.remove(index);
830         setFieldDirty(FieldAlarms);
831         updated();
832     }
833 }
834 
clearAlarms()835 void Incidence::clearAlarms()
836 {
837     update();
838     d->mAlarms.clear();
839     setFieldDirty(FieldAlarms);
840     updated();
841 }
842 
hasEnabledAlarms() const843 bool Incidence::hasEnabledAlarms() const
844 {
845     return std::any_of(d->mAlarms.cbegin(), d->mAlarms.cend(), [](const Alarm::Ptr &alarm) {
846         return alarm->enabled();
847     });
848 }
849 
conferences() const850 Conference::List Incidence::conferences() const
851 {
852     return d->mConferences;
853 }
854 
addConference(const Conference & conference)855 void Incidence::addConference(const Conference &conference)
856 {
857     update();
858     d->mConferences.push_back(conference);
859     setFieldDirty(FieldConferences);
860     updated();
861 }
862 
setConferences(const Conference::List & conferences)863 void Incidence::setConferences(const Conference::List &conferences)
864 {
865     update();
866     d->mConferences = conferences;
867     setFieldDirty(FieldConferences);
868     updated();
869 }
870 
clearConferences()871 void Incidence::clearConferences()
872 {
873     update();
874     d->mConferences.clear();
875     setFieldDirty(FieldConferences);
876     updated();
877 }
878 
setLocation(const QString & location,bool isRich)879 void Incidence::setLocation(const QString &location, bool isRich)
880 {
881     if (mReadOnly) {
882         return;
883     }
884 
885     if (d->mLocation != location || d->mLocationIsRich != isRich) {
886         update();
887         d->mLocation = location;
888         d->mLocationIsRich = isRich;
889         setFieldDirty(FieldLocation);
890         updated();
891     }
892 }
893 
setLocation(const QString & location)894 void Incidence::setLocation(const QString &location)
895 {
896     setLocation(location, Qt::mightBeRichText(location));
897 }
898 
location() const899 QString Incidence::location() const
900 {
901     return d->mLocation;
902 }
903 
richLocation() const904 QString Incidence::richLocation() const
905 {
906     if (locationIsRich()) {
907         return d->mLocation;
908     } else {
909         return d->mLocation.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>"));
910     }
911 }
912 
locationIsRich() const913 bool Incidence::locationIsRich() const
914 {
915     return d->mLocationIsRich;
916 }
917 
setSchedulingID(const QString & sid,const QString & uid)918 void Incidence::setSchedulingID(const QString &sid, const QString &uid)
919 {
920     if (!uid.isEmpty()) {
921         setUid(uid);
922     }
923     if (sid != d->mSchedulingID) {
924         update();
925         d->mSchedulingID = sid;
926         setFieldDirty(FieldSchedulingId);
927         updated();
928     }
929 }
930 
schedulingID() const931 QString Incidence::schedulingID() const
932 {
933     if (d->mSchedulingID.isNull()) {
934         // Nothing set, so use the normal uid
935         return uid();
936     }
937     return d->mSchedulingID;
938 }
939 
hasGeo() const940 bool Incidence::hasGeo() const
941 {
942     // For internal consistency, return false if either coordinate is invalid.
943     return d->mGeoLatitude != INVALID_LATLON && d->mGeoLongitude != INVALID_LATLON;
944 }
945 
setHasGeo(bool hasGeo)946 void Incidence::setHasGeo(bool hasGeo)
947 {
948     if (mReadOnly) {
949         return;
950     }
951 
952     if (!hasGeo) {
953         update();
954         d->mGeoLatitude = INVALID_LATLON;
955         d->mGeoLongitude = INVALID_LATLON;
956         setFieldDirty(FieldGeoLatitude);
957         setFieldDirty(FieldGeoLongitude);
958         updated();
959     }
960     // If hasGeo is true, the caller should set latitude and longitude to legal values..
961 }
962 
geoLatitude() const963 float Incidence::geoLatitude() const
964 {
965     // For internal consistency, both coordinates are considered invalid if either is.
966     return (INVALID_LATLON == d->mGeoLongitude) ? INVALID_LATLON : d->mGeoLatitude;
967 }
968 
setGeoLatitude(float geolatitude)969 void Incidence::setGeoLatitude(float geolatitude)
970 {
971     if (mReadOnly) {
972         return;
973     }
974 
975     if (isnan(geolatitude)) {
976         geolatitude = INVALID_LATLON;
977     }
978     if (geolatitude != INVALID_LATLON && (geolatitude < -90.0 || geolatitude > 90.0)) {
979         qCWarning(KCALCORE_LOG) << "Ignoring invalid  latitude" << geolatitude;
980         return;
981     }
982 
983     update();
984     d->mGeoLatitude = geolatitude;
985     setFieldDirty(FieldGeoLatitude);
986     updated();
987 }
988 
geoLongitude() const989 float Incidence::geoLongitude() const
990 {
991     // For internal consistency, both coordinates are considered invalid if either is.
992     return (INVALID_LATLON == d->mGeoLatitude) ? INVALID_LATLON : d->mGeoLongitude;
993 }
994 
setGeoLongitude(float geolongitude)995 void Incidence::setGeoLongitude(float geolongitude)
996 {
997     if (mReadOnly) {
998         return;
999     }
1000 
1001     if (isnan(geolongitude)) {
1002         geolongitude = INVALID_LATLON;
1003     }
1004     if (geolongitude != INVALID_LATLON && (geolongitude < -180.0 || geolongitude > 180.0)) {
1005         qCWarning(KCALCORE_LOG) << "Ignoring invalid  longitude" << geolongitude;
1006         return;
1007     }
1008 
1009     update();
1010     d->mGeoLongitude = geolongitude;
1011     setFieldDirty(FieldGeoLongitude);
1012     updated();
1013 }
1014 
hasRecurrenceId() const1015 bool Incidence::hasRecurrenceId() const
1016 {
1017     return (allDay() && d->mRecurrenceId.date().isValid()) || d->mRecurrenceId.isValid();
1018 }
1019 
recurrenceId() const1020 QDateTime Incidence::recurrenceId() const
1021 {
1022     return d->mRecurrenceId;
1023 }
1024 
setThisAndFuture(bool thisAndFuture)1025 void Incidence::setThisAndFuture(bool thisAndFuture)
1026 {
1027     d->mThisAndFuture = thisAndFuture;
1028 }
1029 
thisAndFuture() const1030 bool Incidence::thisAndFuture() const
1031 {
1032     return d->mThisAndFuture;
1033 }
1034 
setRecurrenceId(const QDateTime & recurrenceId)1035 void Incidence::setRecurrenceId(const QDateTime &recurrenceId)
1036 {
1037     if (!mReadOnly) {
1038         update();
1039         d->mRecurrenceId = recurrenceId;
1040         setFieldDirty(FieldRecurrenceId);
1041         updated();
1042     }
1043 }
1044 
1045 /** Observer interface for the recurrence class. If the recurrence is changed,
1046     this method will be called for the incidence the recurrence object
1047     belongs to. */
recurrenceUpdated(Recurrence * recurrence)1048 void Incidence::recurrenceUpdated(Recurrence *recurrence)
1049 {
1050     if (recurrence == d->mRecurrence) {
1051         update();
1052         setFieldDirty(FieldRecurrence);
1053         updated();
1054     }
1055 }
1056 
1057 //@cond PRIVATE
1058 #define ALT_DESC_FIELD "X-ALT-DESC"
1059 #define ALT_DESC_PARAMETERS QStringLiteral("FMTTYPE=text/html")
1060 //@endcond
1061 
hasAltDescription() const1062 bool Incidence::hasAltDescription() const
1063 {
1064     const QString value = nonKDECustomProperty(ALT_DESC_FIELD);
1065     const QString parameter = nonKDECustomPropertyParameters(ALT_DESC_FIELD);
1066 
1067     return parameter == ALT_DESC_PARAMETERS && !value.isEmpty();
1068 }
1069 
setAltDescription(const QString & altdescription)1070 void Incidence::setAltDescription(const QString &altdescription)
1071 {
1072     if (altdescription.isEmpty()) {
1073         removeNonKDECustomProperty(ALT_DESC_FIELD);
1074     } else {
1075         setNonKDECustomProperty(ALT_DESC_FIELD, altdescription, ALT_DESC_PARAMETERS);
1076     }
1077 }
1078 
altDescription() const1079 QString Incidence::altDescription() const
1080 {
1081     if (!hasAltDescription()) {
1082         return QString();
1083     } else {
1084         return nonKDECustomProperty(ALT_DESC_FIELD);
1085     }
1086 }
1087 
1088 /** static */
mimeTypes()1089 QStringList Incidence::mimeTypes()
1090 {
1091     return QStringList() << QStringLiteral("text/calendar") << KCalendarCore::Event::eventMimeType() << KCalendarCore::Todo::todoMimeType()
1092                          << KCalendarCore::Journal::journalMimeType();
1093 }
1094 
serialize(QDataStream & out) const1095 void Incidence::serialize(QDataStream &out) const
1096 {
1097     serializeQDateTimeAsKDateTime(out, d->mCreated);
1098     out << d->mRevision << d->mDescription << d->mDescriptionIsRich << d->mSummary << d->mSummaryIsRich << d->mLocation << d->mLocationIsRich << d->mCategories
1099         << d->mResources << d->mStatusString << d->mPriority << d->mSchedulingID << d->mGeoLatitude << d->mGeoLongitude
1100         << hasGeo();    // No longer used, but serialized/deserialized for compatibility.
1101     serializeQDateTimeAsKDateTime(out, d->mRecurrenceId);
1102     out << d->mThisAndFuture << d->mLocalOnly << d->mStatus << d->mSecrecy << (d->mRecurrence ? true : false) << d->mAttachments.count() << d->mAlarms.count()
1103         << d->mConferences.count() << d->mRelatedToUid;
1104 
1105     if (d->mRecurrence) {
1106         out << d->mRecurrence;
1107     }
1108 
1109     for (const Attachment &attachment : qAsConst(d->mAttachments)) {
1110         out << attachment;
1111     }
1112 
1113     for (const Alarm::Ptr &alarm : qAsConst(d->mAlarms)) {
1114         out << alarm;
1115     }
1116 
1117     for (const Conference &conf : qAsConst(d->mConferences)) {
1118         out << conf;
1119     }
1120 }
1121 
deserialize(QDataStream & in)1122 void Incidence::deserialize(QDataStream &in)
1123 {
1124     bool hasGeo;    // No longer used, but serialized/deserialized for compatibility.
1125     quint32 status;
1126     quint32 secrecy;
1127     bool hasRecurrence;
1128     int attachmentCount;
1129     int alarmCount;
1130     int conferencesCount;
1131     QMap<int, QString> relatedToUid;
1132     deserializeKDateTimeAsQDateTime(in, d->mCreated);
1133     in >> d->mRevision >> d->mDescription >> d->mDescriptionIsRich >> d->mSummary >> d->mSummaryIsRich >> d->mLocation >> d->mLocationIsRich >> d->mCategories
1134         >> d->mResources >> d->mStatusString >> d->mPriority >> d->mSchedulingID >> d->mGeoLatitude >> d->mGeoLongitude >> hasGeo;
1135     deserializeKDateTimeAsQDateTime(in, d->mRecurrenceId);
1136     in >> d->mThisAndFuture >> d->mLocalOnly >> status >> secrecy >> hasRecurrence >> attachmentCount >> alarmCount >> conferencesCount >> relatedToUid;
1137 
1138     if (hasRecurrence) {
1139         d->mRecurrence = new Recurrence();
1140         d->mRecurrence->addObserver(const_cast<KCalendarCore::Incidence *>(this));
1141         in >> d->mRecurrence;
1142     }
1143 
1144     d->mAttachments.clear();
1145     d->mAlarms.clear();
1146     d->mConferences.clear();
1147 
1148     d->mAttachments.reserve(attachmentCount);
1149     for (int i = 0; i < attachmentCount; ++i) {
1150         Attachment attachment;
1151         in >> attachment;
1152         d->mAttachments.append(attachment);
1153     }
1154 
1155     d->mAlarms.reserve(alarmCount);
1156     for (int i = 0; i < alarmCount; ++i) {
1157         Alarm::Ptr alarm = Alarm::Ptr(new Alarm(this));
1158         in >> alarm;
1159         d->mAlarms.append(alarm);
1160     }
1161 
1162     d->mConferences.reserve(conferencesCount);
1163     for (int i = 0; i < conferencesCount; ++i) {
1164         Conference conf;
1165         in >> conf;
1166         d->mConferences.push_back(conf);
1167     }
1168 
1169     d->mStatus = static_cast<Incidence::Status>(status);
1170     d->mSecrecy = static_cast<Incidence::Secrecy>(secrecy);
1171 
1172     d->mRelatedToUid.clear();
1173 
1174     auto it = relatedToUid.cbegin();
1175     auto end = relatedToUid.cend();
1176     for (; it != end; ++it) {
1177         d->mRelatedToUid.insert(static_cast<Incidence::RelType>(it.key()), it.value());
1178     }
1179 }
1180 
1181 namespace
1182 {
1183 template<typename T>
toVariantList(int size,typename QVector<T>::ConstIterator begin,typename QVector<T>::ConstIterator end)1184 QVariantList toVariantList(int size, typename QVector<T>::ConstIterator begin, typename QVector<T>::ConstIterator end)
1185 {
1186     QVariantList l;
1187     l.reserve(size);
1188     std::transform(begin, end, std::back_inserter(l), [](const T &val) {
1189         return QVariant::fromValue(val);
1190     });
1191     return l;
1192 }
1193 
1194 } // namespace
1195 
attachmentsVariant() const1196 QVariantList Incidence::attachmentsVariant() const
1197 {
1198     return toVariantList<Attachment>(d->mAttachments.size(), d->mAttachments.cbegin(), d->mAttachments.cend());
1199 }
1200 
conferencesVariant() const1201 QVariantList Incidence::conferencesVariant() const
1202 {
1203     return toVariantList<Conference>(d->mConferences.size(), d->mConferences.cbegin(), d->mConferences.cend());
1204 }
1205