File indexing completed on 2024-12-01 04:47:51

0001 /*
0002     This file is part of the kolab resource - the implementation of the
0003     Kolab storage format. See www.kolab.org for documentation on this.
0004 
0005     SPDX-FileCopyrightText: 2004 Bo Thorsen <bo@sonofthor.dk>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "incidence.h"
0011 #include "libkolab-version.h"
0012 #include "pimkolab_debug.h"
0013 
0014 #include <QList>
0015 
0016 #include <QBitArray>
0017 
0018 using namespace KolabV2;
0019 
0020 Incidence::Incidence(const QString &tz, const KCalendarCore::Incidence::Ptr &incidence)
0021     : KolabBase(tz)
0022 {
0023     Q_UNUSED(incidence)
0024 }
0025 
0026 Incidence::~Incidence() = default;
0027 
0028 void Incidence::setPriority(int priority)
0029 {
0030     mPriority = priority;
0031 }
0032 
0033 int Incidence::priority() const
0034 {
0035     return mPriority;
0036 }
0037 
0038 void Incidence::setSummary(const QString &summary)
0039 {
0040     mSummary = summary;
0041 }
0042 
0043 QString Incidence::summary() const
0044 {
0045     return mSummary;
0046 }
0047 
0048 void Incidence::setLocation(const QString &location)
0049 {
0050     mLocation = location;
0051 }
0052 
0053 QString Incidence::location() const
0054 {
0055     return mLocation;
0056 }
0057 
0058 void Incidence::setOrganizer(const Email &organizer)
0059 {
0060     mOrganizer = organizer;
0061 }
0062 
0063 KolabBase::Email Incidence::organizer() const
0064 {
0065     return mOrganizer;
0066 }
0067 
0068 void Incidence::setStartDate(const QDateTime &startDate)
0069 {
0070     mStartDate = startDate;
0071     if (mFloatingStatus == AllDay) {
0072         qCDebug(PIMKOLAB_LOG) << "ERROR: Time on start date but no time on the event";
0073     }
0074     mFloatingStatus = HasTime;
0075 }
0076 
0077 void Incidence::setStartDate(const QDate &startDate)
0078 {
0079     mStartDate = QDateTime(startDate, QTime());
0080     if (mFloatingStatus == HasTime) {
0081         qCDebug(PIMKOLAB_LOG) << "ERROR: No time on start date but time on the event";
0082     }
0083     mFloatingStatus = AllDay;
0084 }
0085 
0086 void Incidence::setStartDate(const QString &startDate)
0087 {
0088     if (startDate.length() > 10) {
0089         // This is a date + time
0090         setStartDate(stringToDateTime(startDate));
0091     } else {
0092         // This is only a date
0093         setStartDate(stringToDate(startDate));
0094     }
0095 }
0096 
0097 QDateTime Incidence::startDate() const
0098 {
0099     return mStartDate;
0100 }
0101 
0102 void Incidence::setAlarm(float alarm)
0103 {
0104     mAlarm = alarm;
0105     mHasAlarm = true;
0106 }
0107 
0108 float Incidence::alarm() const
0109 {
0110     return mAlarm;
0111 }
0112 
0113 Incidence::Recurrence Incidence::recurrence() const
0114 {
0115     return mRecurrence;
0116 }
0117 
0118 void Incidence::addAttendee(const Attendee &attendee)
0119 {
0120     mAttendees.append(attendee);
0121 }
0122 
0123 QList<Incidence::Attendee> &Incidence::attendees()
0124 {
0125     return mAttendees;
0126 }
0127 
0128 const QList<Incidence::Attendee> &Incidence::attendees() const
0129 {
0130     return mAttendees;
0131 }
0132 
0133 void Incidence::setInternalUID(const QString &iuid)
0134 {
0135     mInternalUID = iuid;
0136 }
0137 
0138 QString Incidence::internalUID() const
0139 {
0140     return mInternalUID;
0141 }
0142 
0143 bool Incidence::loadAttendeeAttribute(QDomElement &element, Attendee &attendee)
0144 {
0145     for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) {
0146         if (n.isComment()) {
0147             continue;
0148         }
0149         if (n.isElement()) {
0150             QDomElement e = n.toElement();
0151             const QString tagName = e.tagName();
0152 
0153             if (tagName == QLatin1StringView("display-name")) {
0154                 attendee.displayName = e.text();
0155             } else if (tagName == QLatin1StringView("smtp-address")) {
0156                 attendee.smtpAddress = e.text();
0157             } else if (tagName == QLatin1StringView("status")) {
0158                 attendee.status = e.text();
0159             } else if (tagName == QLatin1StringView("request-response")) {
0160                 // This sets reqResp to false, if the text is "false". Otherwise it
0161                 // sets it to true. This means the default setting is true.
0162                 attendee.requestResponse = (e.text().toLower() != QLatin1StringView("false"));
0163             } else if (tagName == QLatin1StringView("invitation-sent")) {
0164                 // Like above, only this defaults to false
0165                 attendee.invitationSent = (e.text().toLower() != QLatin1StringView("true"));
0166             } else if (tagName == QLatin1StringView("role")) {
0167                 attendee.role = e.text();
0168             } else if (tagName == QLatin1StringView("delegated-to")) {
0169                 attendee.delegate = e.text();
0170             } else if (tagName == QLatin1StringView("delegated-from")) {
0171                 attendee.delegator = e.text();
0172             } else {
0173                 // TODO: Unhandled tag - save for later storage
0174                 qCDebug(PIMKOLAB_LOG) << "Warning: Unhandled tag" << e.tagName();
0175             }
0176         } else {
0177             qCDebug(PIMKOLAB_LOG) << "Node is not a comment or an element???";
0178         }
0179     }
0180 
0181     return true;
0182 }
0183 
0184 void Incidence::saveAttendeeAttribute(QDomElement &element, const Attendee &attendee) const
0185 {
0186     QDomElement e = element.ownerDocument().createElement(QStringLiteral("attendee"));
0187     element.appendChild(e);
0188     writeString(e, QStringLiteral("display-name"), attendee.displayName);
0189     writeString(e, QStringLiteral("smtp-address"), attendee.smtpAddress);
0190     writeString(e, QStringLiteral("status"), attendee.status);
0191     writeString(e, QStringLiteral("request-response"), (attendee.requestResponse ? QStringLiteral("true") : QStringLiteral("false")));
0192     writeString(e, QStringLiteral("invitation-sent"), (attendee.invitationSent ? QStringLiteral("true") : QStringLiteral("false")));
0193     writeString(e, QStringLiteral("role"), attendee.role);
0194     writeString(e, QStringLiteral("delegated-to"), attendee.delegate);
0195     writeString(e, QStringLiteral("delegated-from"), attendee.delegator);
0196 }
0197 
0198 void Incidence::saveAttendees(QDomElement &element) const
0199 {
0200     for (const Attendee &attendee : std::as_const(mAttendees)) {
0201         saveAttendeeAttribute(element, attendee);
0202     }
0203 }
0204 
0205 void Incidence::saveAttachments(QDomElement &element) const
0206 {
0207     for (const KCalendarCore::Attachment &a : std::as_const(mAttachments)) {
0208         if (a.isUri()) {
0209             writeString(element, QStringLiteral("link-attachment"), a.uri());
0210         } else if (a.isBinary()) {
0211             writeString(element, QStringLiteral("inline-attachment"), a.label());
0212         }
0213     }
0214 }
0215 
0216 void Incidence::saveAlarms(QDomElement &element) const
0217 {
0218     if (mAlarms.isEmpty()) {
0219         return;
0220     }
0221 
0222     QDomElement list = element.ownerDocument().createElement(QStringLiteral("advanced-alarms"));
0223     element.appendChild(list);
0224     for (const KCalendarCore::Alarm::Ptr &a : std::as_const(mAlarms)) {
0225         QDomElement e = list.ownerDocument().createElement(QStringLiteral("alarm"));
0226         list.appendChild(e);
0227 
0228         writeString(e, QStringLiteral("enabled"), a->enabled() ? QStringLiteral("1") : QStringLiteral("0"));
0229         if (a->hasStartOffset()) {
0230             writeString(e, QStringLiteral("start-offset"), QString::number(a->startOffset().asSeconds() / 60));
0231         }
0232         if (a->hasEndOffset()) {
0233             writeString(e, QStringLiteral("end-offset"), QString::number(a->endOffset().asSeconds() / 60));
0234         }
0235         if (a->repeatCount()) {
0236             writeString(e, QStringLiteral("repeat-count"), QString::number(a->repeatCount()));
0237             writeString(e, QStringLiteral("repeat-interval"), QString::number(a->snoozeTime().asSeconds()));
0238         }
0239 
0240         switch (a->type()) {
0241         case KCalendarCore::Alarm::Invalid:
0242             break;
0243         case KCalendarCore::Alarm::Display:
0244             e.setAttribute(QStringLiteral("type"), QStringLiteral("display"));
0245             writeString(e, QStringLiteral("text"), a->text());
0246             break;
0247         case KCalendarCore::Alarm::Procedure:
0248             e.setAttribute(QStringLiteral("type"), QStringLiteral("procedure"));
0249             writeString(e, QStringLiteral("program"), a->programFile());
0250             writeString(e, QStringLiteral("arguments"), a->programArguments());
0251             break;
0252         case KCalendarCore::Alarm::Email: {
0253             e.setAttribute(QStringLiteral("type"), QStringLiteral("email"));
0254             QDomElement addresses = e.ownerDocument().createElement(QStringLiteral("addresses"));
0255             e.appendChild(addresses);
0256             const auto mailAddresses{a->mailAddresses()};
0257             for (const KCalendarCore::Person &person : mailAddresses) {
0258                 writeString(addresses, QStringLiteral("address"), person.fullName());
0259             }
0260             writeString(e, QStringLiteral("subject"), a->mailSubject());
0261             writeString(e, QStringLiteral("mail-text"), a->mailText());
0262             QDomElement attachments = e.ownerDocument().createElement(QStringLiteral("attachments"));
0263             e.appendChild(attachments);
0264             const auto mailAttachments{a->mailAttachments()};
0265             for (const QString &attachment : mailAttachments) {
0266                 writeString(attachments, QStringLiteral("attachment"), attachment);
0267             }
0268             break;
0269         }
0270         case KCalendarCore::Alarm::Audio:
0271             e.setAttribute(QStringLiteral("type"), QStringLiteral("audio"));
0272             writeString(e, QStringLiteral("file"), a->audioFile());
0273             break;
0274         default:
0275             qCWarning(PIMKOLAB_LOG) << "Unhandled alarm type:" << a->type();
0276             break;
0277         }
0278     }
0279 }
0280 
0281 void Incidence::saveRecurrence(QDomElement &element) const
0282 {
0283     QDomElement e = element.ownerDocument().createElement(QStringLiteral("recurrence"));
0284     element.appendChild(e);
0285     e.setAttribute(QStringLiteral("cycle"), mRecurrence.cycle);
0286     if (!mRecurrence.type.isEmpty()) {
0287         e.setAttribute(QStringLiteral("type"), mRecurrence.type);
0288     }
0289     writeString(e, QStringLiteral("interval"), QString::number(mRecurrence.interval));
0290     const auto days{mRecurrence.days};
0291     for (const QString &recurrence : days) {
0292         writeString(e, QStringLiteral("day"), recurrence);
0293     }
0294     if (!mRecurrence.dayNumber.isEmpty()) {
0295         writeString(e, QStringLiteral("daynumber"), mRecurrence.dayNumber);
0296     }
0297     if (!mRecurrence.month.isEmpty()) {
0298         writeString(e, QStringLiteral("month"), mRecurrence.month);
0299     }
0300     if (!mRecurrence.rangeType.isEmpty()) {
0301         QDomElement range = element.ownerDocument().createElement(QStringLiteral("range"));
0302         e.appendChild(range);
0303         range.setAttribute(QStringLiteral("type"), mRecurrence.rangeType);
0304         QDomText t = element.ownerDocument().createTextNode(mRecurrence.range);
0305         range.appendChild(t);
0306     }
0307     const auto exclusions{mRecurrence.exclusions};
0308     for (const QDate &date : exclusions) {
0309         writeString(e, QStringLiteral("exclusion"), dateToString(date));
0310     }
0311 }
0312 
0313 void Incidence::loadRecurrence(const QDomElement &element)
0314 {
0315     mRecurrence.interval = 0;
0316     mRecurrence.cycle = element.attribute(QStringLiteral("cycle"));
0317     mRecurrence.type = element.attribute(QStringLiteral("type"));
0318     for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) {
0319         if (n.isComment()) {
0320             continue;
0321         }
0322         if (n.isElement()) {
0323             QDomElement e = n.toElement();
0324             QString tagName = e.tagName();
0325             if (tagName == QLatin1StringView("interval")) {
0326                 // kolab/issue4229, sometimes  the interval value can be empty
0327                 if (e.text().isEmpty() || e.text().toInt() <= 0) {
0328                     mRecurrence.interval = 1;
0329                 } else {
0330                     mRecurrence.interval = e.text().toInt();
0331                 }
0332             } else if (tagName == QLatin1StringView("day")) { // can be present multiple times
0333                 mRecurrence.days.append(e.text());
0334             } else if (tagName == QLatin1StringView("daynumber")) {
0335                 mRecurrence.dayNumber = e.text();
0336             } else if (tagName == QLatin1StringView("month")) {
0337                 mRecurrence.month = e.text();
0338             } else if (tagName == QLatin1StringView("range")) {
0339                 mRecurrence.rangeType = e.attribute(QStringLiteral("type"));
0340                 mRecurrence.range = e.text();
0341             } else if (tagName == QLatin1StringView("exclusion")) {
0342                 mRecurrence.exclusions.append(stringToDate(e.text()));
0343             } else {
0344                 // TODO: Unhandled tag - save for later storage
0345                 qCDebug(PIMKOLAB_LOG) << "Warning: Unhandled tag" << e.tagName();
0346             }
0347         }
0348     }
0349 }
0350 
0351 static void loadAddressesHelper(const QDomElement &element, const KCalendarCore::Alarm::Ptr &a)
0352 {
0353     for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) {
0354         if (n.isComment()) {
0355             continue;
0356         }
0357         if (n.isElement()) {
0358             QDomElement e = n.toElement();
0359             QString tagName = e.tagName();
0360 
0361             if (tagName == QLatin1StringView("address")) {
0362                 a->addMailAddress(KCalendarCore::Person::fromFullName(e.text()));
0363             } else {
0364                 qCWarning(PIMKOLAB_LOG) << "Unhandled tag" << tagName;
0365             }
0366         }
0367     }
0368 }
0369 
0370 static void loadAttachmentsHelper(const QDomElement &element, const KCalendarCore::Alarm::Ptr &a)
0371 {
0372     for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) {
0373         if (n.isComment()) {
0374             continue;
0375         }
0376         if (n.isElement()) {
0377             QDomElement e = n.toElement();
0378             QString tagName = e.tagName();
0379 
0380             if (tagName == QLatin1StringView("attachment")) {
0381                 a->addMailAttachment(e.text());
0382             } else {
0383                 qCWarning(PIMKOLAB_LOG) << "Unhandled tag" << tagName;
0384             }
0385         }
0386     }
0387 }
0388 
0389 static void loadAlarmHelper(const QDomElement &element, const KCalendarCore::Alarm::Ptr &a)
0390 {
0391     for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) {
0392         if (n.isComment()) {
0393             continue;
0394         }
0395         if (n.isElement()) {
0396             QDomElement e = n.toElement();
0397             QString tagName = e.tagName();
0398 
0399             if (tagName == QLatin1StringView("start-offset")) {
0400                 a->setStartOffset(e.text().toInt() * 60);
0401             } else if (tagName == QLatin1StringView("end-offset")) {
0402                 a->setEndOffset(e.text().toInt() * 60);
0403             } else if (tagName == QLatin1StringView("repeat-count")) {
0404                 a->setRepeatCount(e.text().toInt());
0405             } else if (tagName == QLatin1StringView("repeat-interval")) {
0406                 a->setSnoozeTime(e.text().toInt());
0407             } else if (tagName == QLatin1StringView("text")) {
0408                 a->setText(e.text());
0409             } else if (tagName == QLatin1StringView("program")) {
0410                 a->setProgramFile(e.text());
0411             } else if (tagName == QLatin1StringView("arguments")) {
0412                 a->setProgramArguments(e.text());
0413             } else if (tagName == QLatin1StringView("addresses")) {
0414                 loadAddressesHelper(e, a);
0415             } else if (tagName == QLatin1StringView("subject")) {
0416                 a->setMailSubject(e.text());
0417             } else if (tagName == QLatin1StringView("mail-text")) {
0418                 a->setMailText(e.text());
0419             } else if (tagName == QLatin1StringView("attachments")) {
0420                 loadAttachmentsHelper(e, a);
0421             } else if (tagName == QLatin1StringView("file")) {
0422                 a->setAudioFile(e.text());
0423             } else if (tagName == QLatin1StringView("enabled")) {
0424                 a->setEnabled(e.text().toInt() != 0);
0425             } else {
0426                 qCWarning(PIMKOLAB_LOG) << "Unhandled tag" << tagName;
0427             }
0428         }
0429     }
0430 }
0431 
0432 void Incidence::loadAlarms(const QDomElement &element)
0433 {
0434     for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) {
0435         if (n.isComment()) {
0436             continue;
0437         }
0438         if (n.isElement()) {
0439             QDomElement e = n.toElement();
0440             QString tagName = e.tagName();
0441 
0442             if (tagName == QLatin1StringView("alarm")) {
0443                 KCalendarCore::Alarm::Ptr a = KCalendarCore::Alarm::Ptr(new KCalendarCore::Alarm(nullptr));
0444                 a->setEnabled(true); // default to enabled, unless some XML attribute says otherwise.
0445                 QString type = e.attribute(QStringLiteral("type"));
0446                 if (type == QLatin1StringView("display")) {
0447                     a->setType(KCalendarCore::Alarm::Display);
0448                 } else if (type == QLatin1StringView("procedure")) {
0449                     a->setType(KCalendarCore::Alarm::Procedure);
0450                 } else if (type == QLatin1StringView("email")) {
0451                     a->setType(KCalendarCore::Alarm::Email);
0452                 } else if (type == QLatin1StringView("audio")) {
0453                     a->setType(KCalendarCore::Alarm::Audio);
0454                 } else {
0455                     qCWarning(PIMKOLAB_LOG) << "Unhandled alarm type:" << type;
0456                 }
0457 
0458                 loadAlarmHelper(e, a);
0459                 mAlarms << a;
0460             } else {
0461                 qCWarning(PIMKOLAB_LOG) << "Unhandled tag" << tagName;
0462             }
0463         }
0464     }
0465 }
0466 
0467 bool Incidence::loadAttribute(QDomElement &element)
0468 {
0469     QString tagName = element.tagName();
0470 
0471     if (tagName == QLatin1StringView("priority")) {
0472         bool ok;
0473         int p = element.text().toInt(&ok);
0474         if (!ok || p < 1 || p > 9) {
0475             qCWarning(PIMKOLAB_LOG) << "Invalid \"priority\" value:" << element.text();
0476         } else {
0477             setPriority(p);
0478         }
0479     } else if (tagName == QLatin1StringView("x-kcal-priority")) { // for backwards compat
0480         bool ok;
0481         int p = element.text().toInt(&ok);
0482         if (!ok || p < 0 || p > 9) {
0483             qCWarning(PIMKOLAB_LOG) << "Invalid \"x-kcal-priority\" value:" << element.text();
0484         } else {
0485             if (priority() == 0) {
0486                 setPriority(p);
0487             }
0488         }
0489     } else if (tagName == QLatin1StringView("summary")) {
0490         setSummary(element.text());
0491     } else if (tagName == QLatin1StringView("location")) {
0492         setLocation(element.text());
0493     } else if (tagName == QLatin1StringView("organizer")) {
0494         Email email;
0495         if (loadEmailAttribute(element, email)) {
0496             setOrganizer(email);
0497             return true;
0498         } else {
0499             return false;
0500         }
0501     } else if (tagName == QLatin1StringView("start-date")) {
0502         setStartDate(element.text());
0503     } else if (tagName == QLatin1StringView("recurrence")) {
0504         loadRecurrence(element);
0505     } else if (tagName == QLatin1StringView("attendee")) {
0506         Attendee attendee;
0507         if (loadAttendeeAttribute(element, attendee)) {
0508             addAttendee(attendee);
0509             return true;
0510         } else {
0511             return false;
0512         }
0513     } else if (tagName == QLatin1StringView("link-attachment")) {
0514         mAttachments.push_back(KCalendarCore::Attachment(element.text()));
0515     } else if (tagName == QLatin1StringView("alarm")) {
0516         // Alarms should be minutes before. Libkcal uses event time + alarm time
0517         setAlarm(-element.text().toInt());
0518     } else if (tagName == QLatin1StringView("advanced-alarms")) {
0519         loadAlarms(element);
0520     } else if (tagName == QLatin1StringView("x-kde-internaluid")) {
0521         setInternalUID(element.text());
0522     } else if (tagName == QLatin1StringView("x-custom")) {
0523         loadCustomAttributes(element);
0524     } else if (tagName == QLatin1StringView("inline-attachment")) {
0525         // we handle that separately later on, so no need to create a KolabUnhandled entry for it
0526     } else {
0527         bool ok = KolabBase::loadAttribute(element);
0528         if (!ok) {
0529             // Unhandled tag - save for later storage
0530             qCDebug(PIMKOLAB_LOG) << "Saving unhandled tag" << element.tagName();
0531             Custom c;
0532             c.key = QByteArray("X-KDE-KolabUnhandled-") + element.tagName().toLatin1();
0533             c.value = element.text();
0534             mCustomList.append(c);
0535         }
0536     }
0537     // We handled this
0538     return true;
0539 }
0540 
0541 bool Incidence::saveAttributes(QDomElement &element) const
0542 {
0543     // Save the base class elements
0544     KolabBase::saveAttributes(element);
0545 
0546     if (priority() != 0) {
0547         writeString(element, QStringLiteral("priority"), QString::number(priority()));
0548     }
0549 
0550     if (mFloatingStatus == HasTime) {
0551         writeString(element, QStringLiteral("start-date"), dateTimeToString(startDate()));
0552     } else {
0553         writeString(element, QStringLiteral("start-date"), dateToString(startDate().date()));
0554     }
0555     writeString(element, QStringLiteral("summary"), summary());
0556     writeString(element, QStringLiteral("location"), location());
0557     saveEmailAttribute(element, organizer(), QStringLiteral("organizer"));
0558     if (!mRecurrence.cycle.isEmpty()) {
0559         saveRecurrence(element);
0560     }
0561     saveAttendees(element);
0562     saveAttachments(element);
0563     if (mHasAlarm) {
0564         // Alarms should be minutes before. Libkcal uses event time + alarm time
0565         int alarmTime = qRound(-alarm());
0566         writeString(element, QStringLiteral("alarm"), QString::number(alarmTime));
0567     }
0568     saveAlarms(element);
0569     writeString(element, QStringLiteral("x-kde-internaluid"), internalUID());
0570     saveCustomAttributes(element);
0571     return true;
0572 }
0573 
0574 void Incidence::saveCustomAttributes(QDomElement &element) const
0575 {
0576     for (const Custom &custom : std::as_const(mCustomList)) {
0577         QString key(QString::fromUtf8(custom.key));
0578         Q_ASSERT(!key.isEmpty());
0579         if (key.startsWith(QLatin1StringView("X-KDE-KolabUnhandled-"))) {
0580             key = key.mid(strlen("X-KDE-KolabUnhandled-"));
0581             writeString(element, key, custom.value);
0582         } else {
0583             // Let's use attributes so that other tag-preserving-code doesn't need sub-elements
0584             QDomElement e = element.ownerDocument().createElement(QStringLiteral("x-custom"));
0585             element.appendChild(e);
0586             e.setAttribute(QStringLiteral("key"), key);
0587             e.setAttribute(QStringLiteral("value"), custom.value);
0588         }
0589     }
0590 }
0591 
0592 void Incidence::loadCustomAttributes(QDomElement &element)
0593 {
0594     Custom custom;
0595     custom.key = element.attribute(QStringLiteral("key")).toLatin1();
0596     custom.value = element.attribute(QStringLiteral("value"));
0597     mCustomList.append(custom);
0598 }
0599 
0600 static KCalendarCore::Attendee::PartStat attendeeStringToStatus(const QString &s)
0601 {
0602     if (s == QLatin1StringView("none")) {
0603         return KCalendarCore::Attendee::NeedsAction;
0604     }
0605     if (s == QLatin1StringView("tentative")) {
0606         return KCalendarCore::Attendee::Tentative;
0607     }
0608     if (s == QLatin1StringView("declined")) {
0609         return KCalendarCore::Attendee::Declined;
0610     }
0611     if (s == QLatin1StringView("delegated")) {
0612         return KCalendarCore::Attendee::Delegated;
0613     }
0614 
0615     // Default:
0616     return KCalendarCore::Attendee::Accepted;
0617 }
0618 
0619 static QString attendeeStatusToString(KCalendarCore::Attendee::PartStat status)
0620 {
0621     switch (status) {
0622     case KCalendarCore::Attendee::NeedsAction:
0623         return QStringLiteral("none");
0624     case KCalendarCore::Attendee::Accepted:
0625         return QStringLiteral("accepted");
0626     case KCalendarCore::Attendee::Declined:
0627         return QStringLiteral("declined");
0628     case KCalendarCore::Attendee::Tentative:
0629         return QStringLiteral("tentative");
0630     case KCalendarCore::Attendee::Delegated:
0631         return QStringLiteral("delegated");
0632     case KCalendarCore::Attendee::Completed:
0633     case KCalendarCore::Attendee::InProcess:
0634         // These don't have any meaning in the Kolab format, so just use:
0635         return QStringLiteral("accepted");
0636     default:
0637         // Default for the case that there are more added later:
0638         return QStringLiteral("accepted");
0639     }
0640 }
0641 
0642 static KCalendarCore::Attendee::Role attendeeStringToRole(const QString &s)
0643 {
0644     if (s == QLatin1StringView("optional")) {
0645         return KCalendarCore::Attendee::OptParticipant;
0646     }
0647     if (s == QLatin1StringView("resource")) {
0648         return KCalendarCore::Attendee::NonParticipant;
0649     }
0650     return KCalendarCore::Attendee::ReqParticipant;
0651 }
0652 
0653 static QString attendeeRoleToString(KCalendarCore::Attendee::Role role)
0654 {
0655     switch (role) {
0656     case KCalendarCore::Attendee::ReqParticipant:
0657         return QStringLiteral("required");
0658     case KCalendarCore::Attendee::OptParticipant:
0659         return QStringLiteral("optional");
0660     case KCalendarCore::Attendee::Chair:
0661         // We don't have the notion of chair, so use
0662         return QStringLiteral("required");
0663     case KCalendarCore::Attendee::NonParticipant:
0664         // In Kolab, a non-participant is a resource
0665         return QStringLiteral("resource");
0666     }
0667 
0668     // Default for the case that there are more added later:
0669     return QStringLiteral("required");
0670 }
0671 
0672 static const char *s_weekDayName[] = {
0673     "monday",
0674     "tuesday",
0675     "wednesday",
0676     "thursday",
0677     "friday",
0678     "saturday",
0679     "sunday",
0680 };
0681 
0682 static const char *s_monthName[] = {
0683     "january",
0684     "february",
0685     "march",
0686     "april",
0687     "may",
0688     "june",
0689     "july",
0690     "august",
0691     "september",
0692     "october",
0693     "november",
0694     "december",
0695 };
0696 
0697 void Incidence::setRecurrence(KCalendarCore::Recurrence *recur)
0698 {
0699     mRecurrence.interval = recur->frequency();
0700     switch (recur->recurrenceType()) {
0701     case KCalendarCore::Recurrence::rMinutely: // Not handled by the kolab XML
0702         mRecurrence.cycle = QStringLiteral("minutely");
0703         break;
0704     case KCalendarCore::Recurrence::rHourly: // Not handled by the kolab XML
0705         mRecurrence.cycle = QStringLiteral("hourly");
0706         break;
0707     case KCalendarCore::Recurrence::rDaily:
0708         mRecurrence.cycle = QStringLiteral("daily");
0709         break;
0710     case KCalendarCore::Recurrence::rWeekly: // every X weeks
0711         mRecurrence.cycle = QStringLiteral("weekly");
0712         {
0713             QBitArray arr = recur->days();
0714             for (int idx = 0; idx < 7; ++idx) {
0715                 if (arr.testBit(idx)) {
0716                     mRecurrence.days.append(QString::fromUtf8(s_weekDayName[idx]));
0717                 }
0718             }
0719         }
0720         break;
0721     case KCalendarCore::Recurrence::rMonthlyPos: {
0722         mRecurrence.cycle = QStringLiteral("monthly");
0723         mRecurrence.type = QStringLiteral("weekday");
0724         QList<KCalendarCore::RecurrenceRule::WDayPos> monthPositions = recur->monthPositions();
0725         if (!monthPositions.isEmpty()) {
0726             KCalendarCore::RecurrenceRule::WDayPos monthPos = monthPositions.first();
0727             // TODO: Handle multiple days in the same week
0728             mRecurrence.dayNumber = QString::number(monthPos.pos());
0729             mRecurrence.days.append(QString::fromUtf8(s_weekDayName[monthPos.day() - 1]));
0730             // Not (properly) handled(?): monthPos.negative (nth days before end of month)
0731         }
0732         break;
0733     }
0734     case KCalendarCore::Recurrence::rMonthlyDay: {
0735         mRecurrence.cycle = QStringLiteral("monthly");
0736         mRecurrence.type = QStringLiteral("daynumber");
0737         QList<int> monthDays = recur->monthDays();
0738         // ####### Kolab XML limitation: only the first month day is used
0739         if (!monthDays.isEmpty()) {
0740             mRecurrence.dayNumber = QString::number(monthDays.first());
0741         }
0742         break;
0743     }
0744     case KCalendarCore::Recurrence::rYearlyMonth: // (day n of Month Y)
0745     {
0746         mRecurrence.cycle = QStringLiteral("yearly");
0747         mRecurrence.type = QStringLiteral("monthday");
0748         QList<int> rmd = recur->yearDates();
0749         int day = !rmd.isEmpty() ? rmd.first() : recur->startDate().day();
0750         mRecurrence.dayNumber = QString::number(day);
0751         QList<int> months = recur->yearMonths();
0752         if (!months.isEmpty()) {
0753             mRecurrence.month = QString::fromUtf8(s_monthName[months.first() - 1]); // #### Kolab XML limitation: only one month specified
0754         }
0755         break;
0756     }
0757     case KCalendarCore::Recurrence::rYearlyDay: // YearlyDay (day N of the year). Not supported by Outlook
0758         mRecurrence.cycle = QStringLiteral("yearly");
0759         mRecurrence.type = QStringLiteral("yearday");
0760         mRecurrence.dayNumber = QString::number(recur->yearDays().constFirst());
0761         break;
0762     case KCalendarCore::Recurrence::rYearlyPos: // (weekday X of week N of month Y)
0763         mRecurrence.cycle = QStringLiteral("yearly");
0764         mRecurrence.type = QStringLiteral("weekday");
0765         QList<int> months = recur->yearMonths();
0766         if (!months.isEmpty()) {
0767             mRecurrence.month = QString::fromUtf8(s_monthName[months.first() - 1]); // #### Kolab XML limitation: only one month specified
0768         }
0769         QList<KCalendarCore::RecurrenceRule::WDayPos> monthPositions = recur->yearPositions();
0770         if (!monthPositions.isEmpty()) {
0771             KCalendarCore::RecurrenceRule::WDayPos monthPos = monthPositions.first();
0772             // TODO: Handle multiple days in the same week
0773             mRecurrence.dayNumber = QString::number(monthPos.pos());
0774             mRecurrence.days.append(QString::fromUtf8(s_weekDayName[monthPos.day() - 1]));
0775 
0776             // mRecurrence.dayNumber = QString::number( *recur->yearNums().getFirst() );
0777             // Not handled: monthPos.negative (nth days before end of month)
0778         }
0779         break;
0780     }
0781     int howMany = recur->duration();
0782     if (howMany > 0) {
0783         mRecurrence.rangeType = QStringLiteral("number");
0784         mRecurrence.range = QString::number(howMany);
0785     } else if (howMany == 0) {
0786         mRecurrence.rangeType = QStringLiteral("date");
0787         mRecurrence.range = dateToString(recur->endDate());
0788     } else {
0789         mRecurrence.rangeType = QStringLiteral("none");
0790     }
0791 }
0792 
0793 void Incidence::setFields(const KCalendarCore::Incidence::Ptr &incidence)
0794 {
0795     KolabBase::setFields(incidence);
0796 
0797     setPriority(incidence->priority());
0798     if (incidence->allDay()) {
0799         // This is a all-day event. Don't timezone move this one
0800         mFloatingStatus = AllDay;
0801         setStartDate(incidence->dtStart().date());
0802     } else {
0803         mFloatingStatus = HasTime;
0804         setStartDate(localToUTC(incidence->dtStart()));
0805     }
0806 
0807     setSummary(incidence->summary());
0808     setLocation(incidence->location());
0809 
0810     // Alarm
0811     mHasAlarm = false; // Will be set to true, if we actually have one
0812     if (incidence->hasEnabledAlarms()) {
0813         const KCalendarCore::Alarm::List &alarms = incidence->alarms();
0814         if (!alarms.isEmpty()) {
0815             const KCalendarCore::Alarm::Ptr alarm = alarms.first();
0816             if (alarm->hasStartOffset()) {
0817                 int dur = alarm->startOffset().asSeconds();
0818                 setAlarm((float)dur / 60.0);
0819             }
0820         }
0821     }
0822 
0823     if (!incidence->organizer().isEmpty()) {
0824         Email org(incidence->organizer().name(), incidence->organizer().email());
0825         setOrganizer(org);
0826     }
0827 
0828     // Attendees:
0829     const KCalendarCore::Attendee::List attendees = incidence->attendees();
0830     for (const KCalendarCore::Attendee &kcalAttendee : attendees) {
0831         Attendee attendee;
0832 
0833         attendee.displayName = kcalAttendee.name();
0834         attendee.smtpAddress = kcalAttendee.email();
0835         attendee.status = attendeeStatusToString(kcalAttendee.status());
0836         attendee.requestResponse = kcalAttendee.RSVP();
0837         // TODO: KCalendarCore::Attendee::mFlag is not accessible
0838         // attendee.invitationSent = kcalAttendee->mFlag;
0839         // DF: Hmm? mFlag is set to true and never used at all.... Did you mean another field?
0840         attendee.role = attendeeRoleToString(kcalAttendee.role());
0841         attendee.delegate = kcalAttendee.delegate();
0842         attendee.delegator = kcalAttendee.delegator();
0843 
0844         addAttendee(attendee);
0845     }
0846 
0847     mAttachments.clear();
0848 
0849     // Attachments
0850     const KCalendarCore::Attachment::List attachments = incidence->attachments();
0851     mAttachments.reserve(attachments.size());
0852     for (const KCalendarCore::Attachment &a : attachments) {
0853         mAttachments.push_back(a);
0854     }
0855 
0856     mAlarms.clear();
0857 
0858     // Alarms
0859     const KCalendarCore::Alarm::List alarms = incidence->alarms();
0860     mAlarms.reserve(alarms.count());
0861     for (const KCalendarCore::Alarm::Ptr &a : alarms) {
0862         mAlarms.push_back(a);
0863     }
0864 
0865     if (incidence->recurs()) {
0866         setRecurrence(incidence->recurrence());
0867         mRecurrence.exclusions = incidence->recurrence()->exDates();
0868     }
0869 
0870     // Handle the scheduling ID
0871     if (incidence->schedulingID() == incidence->uid()) {
0872         // There is no scheduling ID
0873         setInternalUID(QString()); // krazy:exclude=nullstrassign for old broken gcc
0874     } else {
0875         // We've internally been using a different uid, so save that as the
0876         // temporary (internal) uid and restore the original uid, the one that
0877         // is used in the folder and the outside world
0878         setUid(incidence->schedulingID());
0879         setInternalUID(incidence->uid());
0880     }
0881 
0882     // Unhandled tags and other custom properties (see libkcal/customproperties.h)
0883     const QMap<QByteArray, QString> map = incidence->customProperties();
0884     QMap<QByteArray, QString>::ConstIterator cit = map.cbegin();
0885     QMap<QByteArray, QString>::ConstIterator cend = map.cend();
0886     for (; cit != cend; ++cit) {
0887         Custom c;
0888         c.key = cit.key();
0889         c.value = cit.value();
0890         mCustomList.append(c);
0891     }
0892 }
0893 
0894 static QBitArray daysListToBitArray(const QStringList &days)
0895 {
0896     QBitArray arr(7);
0897     arr.fill(false);
0898     for (const QString &day : days) {
0899         for (int i = 0; i < 7; ++i) {
0900             if (day == QLatin1StringView(s_weekDayName[i])) {
0901                 arr.setBit(i, true);
0902             }
0903         }
0904     }
0905     return arr;
0906 }
0907 
0908 void Incidence::saveTo(const KCalendarCore::Incidence::Ptr &incidence)
0909 {
0910     KolabBase::saveTo(incidence);
0911 
0912     incidence->setPriority(priority());
0913     if (mFloatingStatus == AllDay) {
0914         // This is an all-day event. Don't timezone move this one
0915         incidence->setDtStart(startDate());
0916         incidence->setAllDay(true);
0917     } else {
0918         incidence->setDtStart(utcToLocal(startDate()));
0919         incidence->setAllDay(false);
0920     }
0921 
0922     incidence->setSummary(summary());
0923     incidence->setLocation(location());
0924 
0925     if (mHasAlarm && mAlarms.isEmpty()) {
0926         KCalendarCore::Alarm::Ptr alarm = incidence->newAlarm();
0927         alarm->setStartOffset(qRound(mAlarm * 60.0));
0928         alarm->setEnabled(true);
0929         alarm->setType(KCalendarCore::Alarm::Display);
0930     } else if (!mAlarms.isEmpty()) {
0931         for (const KCalendarCore::Alarm::Ptr &a : std::as_const(mAlarms)) {
0932             a->setParent(incidence.data());
0933             incidence->addAlarm(a);
0934         }
0935     }
0936 
0937     if (organizer().displayName.isEmpty()) {
0938         incidence->setOrganizer(organizer().smtpAddress);
0939     } else {
0940         incidence->setOrganizer(organizer().displayName + QLatin1Char('<') + organizer().smtpAddress + QLatin1Char('>'));
0941     }
0942 
0943     incidence->clearAttendees();
0944     for (const Attendee &attendee : std::as_const(mAttendees)) {
0945         KCalendarCore::Attendee::PartStat status = attendeeStringToStatus(attendee.status);
0946         KCalendarCore::Attendee::Role role = attendeeStringToRole(attendee.role);
0947         KCalendarCore::Attendee a(attendee.displayName, attendee.smtpAddress, attendee.requestResponse, status, role);
0948         a.setDelegate(attendee.delegate);
0949         a.setDelegator(attendee.delegator);
0950         incidence->addAttendee(a);
0951     }
0952 
0953     incidence->clearAttachments();
0954     for (const KCalendarCore::Attachment &a : std::as_const(mAttachments)) {
0955         incidence->addAttachment(a);
0956     }
0957 
0958     if (!mRecurrence.cycle.isEmpty()) {
0959         KCalendarCore::Recurrence *recur = incidence->recurrence(); // yeah, this creates it
0960         // done below recur->setFrequency( mRecurrence.interval );
0961         if (mRecurrence.cycle == QLatin1StringView("minutely")) {
0962             recur->setMinutely(mRecurrence.interval);
0963         } else if (mRecurrence.cycle == QLatin1StringView("hourly")) {
0964             recur->setHourly(mRecurrence.interval);
0965         } else if (mRecurrence.cycle == QLatin1StringView("daily")) {
0966             recur->setDaily(mRecurrence.interval);
0967         } else if (mRecurrence.cycle == QLatin1StringView("weekly")) {
0968             QBitArray rDays = daysListToBitArray(mRecurrence.days);
0969             recur->setWeekly(mRecurrence.interval, rDays);
0970         } else if (mRecurrence.cycle == QLatin1StringView("monthly")) {
0971             recur->setMonthly(mRecurrence.interval);
0972             if (mRecurrence.type == QLatin1StringView("weekday")) {
0973                 recur->addMonthlyPos(mRecurrence.dayNumber.toInt(), daysListToBitArray(mRecurrence.days));
0974             } else if (mRecurrence.type == QLatin1StringView("daynumber")) {
0975                 recur->addMonthlyDate(mRecurrence.dayNumber.toInt());
0976             } else {
0977                 qCWarning(PIMKOLAB_LOG) << "Unhandled monthly recurrence type" << mRecurrence.type;
0978             }
0979         } else if (mRecurrence.cycle == QLatin1StringView("yearly")) {
0980             recur->setYearly(mRecurrence.interval);
0981             if (mRecurrence.type == QLatin1StringView("monthday")) {
0982                 recur->addYearlyDate(mRecurrence.dayNumber.toInt());
0983                 for (int i = 0; i < 12; ++i) {
0984                     if (QLatin1StringView(s_monthName[i]) == mRecurrence.month) {
0985                         recur->addYearlyMonth(i + 1);
0986                     }
0987                 }
0988             } else if (mRecurrence.type == QLatin1StringView("yearday")) {
0989                 recur->addYearlyDay(mRecurrence.dayNumber.toInt());
0990             } else if (mRecurrence.type == QLatin1StringView("weekday")) {
0991                 for (int i = 0; i < 12; ++i) {
0992                     if (QLatin1StringView(s_monthName[i]) == mRecurrence.month) {
0993                         recur->addYearlyMonth(i + 1);
0994                     }
0995                 }
0996                 recur->addYearlyPos(mRecurrence.dayNumber.toInt(), daysListToBitArray(mRecurrence.days));
0997             } else {
0998                 qCWarning(PIMKOLAB_LOG) << "Unhandled yearly recurrence type" << mRecurrence.type;
0999             }
1000         } else {
1001             qCWarning(PIMKOLAB_LOG) << "Unhandled recurrence cycle" << mRecurrence.cycle;
1002         }
1003 
1004         if (mRecurrence.rangeType == QLatin1StringView("number")) {
1005             recur->setDuration(mRecurrence.range.toInt());
1006         } else if (mRecurrence.rangeType == QLatin1StringView("date")) {
1007             recur->setEndDate(stringToDate(mRecurrence.range));
1008         } // "none" is default since tje set*ly methods set infinite recurrence
1009 
1010         incidence->recurrence()->setExDates(mRecurrence.exclusions);
1011     }
1012     /* If we've stored a uid to be used internally instead of the real one
1013      * (to deal with duplicates of events in different folders) before, then
1014      * restore it, so it does not change. Keep the original uid around for
1015      * scheduling purposes. */
1016     if (!internalUID().isEmpty()) {
1017         incidence->setUid(internalUID());
1018         incidence->setSchedulingID(uid());
1019     }
1020 
1021     for (const Custom &custom : std::as_const(mCustomList)) {
1022         incidence->setNonKDECustomProperty(custom.key, custom.value);
1023     }
1024 }
1025 
1026 QString Incidence::productID() const
1027 {
1028     return QStringLiteral("%1, Kolab resource").arg(QStringLiteral(LIBKOLAB_LIB_VERSION_STRING));
1029 }
1030 
1031 // Unhandled KCalendarCore::Incidence fields:
1032 // revision, status (unused), attendee.uid,
1033 // mComments, mReadOnly