File indexing completed on 2024-04-14 03:50:49

0001 /*
0002   This file is part of the kcalcore library.
0003 
0004   SPDX-FileCopyrightText: 1998 Preston Brown <pbrown@kde.org>
0005   SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
0006 
0007   SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 /**
0010   @file
0011   This file is part of the API for handling calendar data and
0012   defines the VCalFormat base class.
0013 
0014   This class implements the vCalendar format. It provides methods for
0015   loading/saving/converting vCalendar format data into the internal
0016   representation as Calendar and Incidences.
0017 
0018   @brief
0019   vCalendar format implementation.
0020 
0021   @author Preston Brown \<pbrown@kde.org\>
0022   @author Cornelius Schumacher \<schumacher@kde.org\>
0023 */
0024 #include "vcalformat.h"
0025 #include "calendar.h"
0026 #include "calformat_p.h"
0027 #include "exceptions.h"
0028 
0029 #include "kcalendarcore_debug.h"
0030 
0031 extern "C" {
0032 #include <libical/vcc.h>
0033 #include <libical/vobject.h>
0034 }
0035 
0036 #include <QBitArray>
0037 #include <QFile>
0038 #include <QTextDocument> // for .toHtmlEscaped() and Qt::mightBeRichText()
0039 #include <QTimeZone>
0040 
0041 using namespace KCalendarCore;
0042 
0043 /**
0044   Private class that helps to provide binary compatibility between releases.
0045   @internal
0046 */
0047 //@cond PRIVATE
0048 template<typename K>
0049 void removeAllVCal(QList<QSharedPointer<K>> &c, const QSharedPointer<K> &x)
0050 {
0051     if (c.count() < 1) {
0052         return;
0053     }
0054 
0055     int cnt = c.count(x);
0056     if (cnt != 1) {
0057         qCritical() << "There number of relatedTos for this incidence is " << cnt << " (there must be 1 relatedTo only)";
0058         Q_ASSERT_X(false, "removeAllVCal", "Count is not 1.");
0059         return;
0060     }
0061 
0062     c.remove(c.indexOf(x));
0063 }
0064 
0065 class KCalendarCore::VCalFormatPrivate : public CalFormatPrivate
0066 {
0067 public:
0068     Calendar::Ptr mCalendar;
0069     Event::List mEventsRelate; // Events with relations
0070     Todo::List mTodosRelate; // To-dos with relations
0071     QSet<QByteArray> mManuallyWrittenExtensionFields; // X- fields that are manually dumped
0072 };
0073 //@endcond
0074 
0075 VCalFormat::VCalFormat()
0076     : CalFormat(new KCalendarCore::VCalFormatPrivate)
0077 {
0078 }
0079 
0080 VCalFormat::~VCalFormat()
0081 {
0082 }
0083 
0084 static void mimeErrorHandler(char *e)
0085 {
0086     qCWarning(KCALCORE_LOG) << "Error parsing vCalendar file:" << e;
0087 }
0088 
0089 bool VCalFormat::load(const Calendar::Ptr &calendar, const QString &fileName)
0090 {
0091     Q_D(VCalFormat);
0092     d->mCalendar = calendar;
0093 
0094     clearException();
0095 
0096     // this is not necessarily only 1 vcal.  Could be many vcals, or include
0097     // a vcard...
0098     registerMimeErrorHandler(&mimeErrorHandler);    // Note: vCalendar error handler provided by libical.
0099     VObject *vcal = Parse_MIME_FromFileName(const_cast<char *>(QFile::encodeName(fileName).data()));
0100     registerMimeErrorHandler(nullptr);
0101 
0102     if (!vcal) {
0103         setException(new Exception(Exception::CalVersionUnknown));
0104         return false;
0105     }
0106 
0107     // any other top-level calendar stuff should be added/initialized here
0108 
0109     // put all vobjects into their proper places
0110     auto savedTimeZoneId = d->mCalendar->timeZoneId();
0111     populate(vcal);
0112     d->mCalendar->setTimeZoneId(savedTimeZoneId);
0113 
0114     // clean up from vcal API stuff
0115     cleanVObjects(vcal);
0116     cleanStrTbl();
0117 
0118     return true;
0119 }
0120 
0121 bool VCalFormat::save(const Calendar::Ptr &calendar, const QString &fileName)
0122 {
0123     Q_UNUSED(calendar);
0124     Q_UNUSED(fileName);
0125     qCWarning(KCALCORE_LOG) << "Saving VCAL is not supported";
0126     return false;
0127 }
0128 
0129 bool VCalFormat::fromRawString(const Calendar::Ptr &calendar, const QByteArray &string)
0130 {
0131     Q_D(VCalFormat);
0132     d->mCalendar = calendar;
0133 
0134     if (!string.size()) {
0135         return false;
0136     }
0137 
0138     VObject *vcal = Parse_MIME(string.data(), string.size());
0139     if (!vcal) {
0140         return false;
0141     }
0142 
0143     VObjectIterator i;
0144     initPropIterator(&i, vcal);
0145 
0146     // put all vobjects into their proper places
0147     auto savedTimeZoneId = d->mCalendar->timeZoneId();
0148     populate(vcal);
0149     d->mCalendar->setTimeZoneId(savedTimeZoneId);
0150 
0151     // clean up from vcal API stuff
0152     cleanVObjects(vcal);
0153     cleanStrTbl();
0154 
0155     return true;
0156 }
0157 
0158 QString VCalFormat::toString(const Calendar::Ptr &calendar)
0159 {
0160     Q_UNUSED(calendar);
0161 
0162     qCWarning(KCALCORE_LOG) << "Exporting into VCAL is not supported";
0163     return {};
0164 }
0165 
0166 Todo::Ptr VCalFormat::VTodoToEvent(VObject *vtodo)
0167 {
0168     Q_D(VCalFormat);
0169     VObject *vo = nullptr;
0170     VObjectIterator voi;
0171     char *s = nullptr;
0172 
0173     Todo::Ptr anEvent(new Todo);
0174 
0175     // creation date
0176     if ((vo = isAPropertyOf(vtodo, VCDCreatedProp)) != nullptr) {
0177         anEvent->setCreated(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))));
0178         deleteStr(s);
0179     }
0180 
0181     // unique id
0182     vo = isAPropertyOf(vtodo, VCUniqueStringProp);
0183     // while the UID property is preferred, it is not required.  We'll use the
0184     // default Event UID if none is given.
0185     if (vo) {
0186         anEvent->setUid(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))));
0187         deleteStr(s);
0188     }
0189 
0190     // last modification date
0191     if ((vo = isAPropertyOf(vtodo, VCLastModifiedProp)) != nullptr) {
0192         anEvent->setLastModified(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))));
0193         deleteStr(s);
0194     } else {
0195         anEvent->setLastModified(QDateTime::currentDateTimeUtc());
0196     }
0197 
0198     // organizer
0199     // if our extension property for the event's ORGANIZER exists, add it.
0200     if ((vo = isAPropertyOf(vtodo, ICOrganizerProp)) != nullptr) {
0201         anEvent->setOrganizer(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))));
0202         deleteStr(s);
0203     } else {
0204         if (d->mCalendar->owner().name() != QLatin1String("Unknown Name")) {
0205             anEvent->setOrganizer(d->mCalendar->owner());
0206         }
0207     }
0208 
0209     // attendees.
0210     initPropIterator(&voi, vtodo);
0211     while (moreIteration(&voi)) {
0212         vo = nextVObject(&voi);
0213         if (strcmp(vObjectName(vo), VCAttendeeProp) == 0) {
0214             Attendee a;
0215             VObject *vp;
0216             s = fakeCString(vObjectUStringZValue(vo));
0217             QString tmpStr = QString::fromUtf8(s);
0218             deleteStr(s);
0219             tmpStr = tmpStr.simplified();
0220             int emailPos1;
0221             if ((emailPos1 = tmpStr.indexOf(QLatin1Char('<'))) > 0) {
0222                 // both email address and name
0223                 int emailPos2 = tmpStr.lastIndexOf(QLatin1Char('>'));
0224                 a = Attendee(tmpStr.left(emailPos1 - 1), tmpStr.mid(emailPos1 + 1, emailPos2 - (emailPos1 + 1)));
0225             } else if (tmpStr.indexOf(QLatin1Char('@')) > 0) {
0226                 // just an email address
0227                 a = Attendee(QString(), tmpStr);
0228             } else {
0229                 // just a name
0230                 // WTF??? Replacing the spaces of a name and using this as email?
0231                 QString email = tmpStr.replace(QLatin1Char(' '), QLatin1Char('.'));
0232                 a = Attendee(tmpStr, email);
0233             }
0234 
0235             // is there an RSVP property?
0236             if ((vp = isAPropertyOf(vo, VCRSVPProp)) != nullptr) {
0237                 a.setRSVP(vObjectStringZValue(vp));
0238             }
0239             // is there a status property?
0240             if ((vp = isAPropertyOf(vo, VCStatusProp)) != nullptr) {
0241                 a.setStatus(readStatus(vObjectStringZValue(vp)));
0242             }
0243             // add the attendee
0244             anEvent->addAttendee(a);
0245         }
0246     }
0247 
0248     // description for todo
0249     if ((vo = isAPropertyOf(vtodo, VCDescriptionProp)) != nullptr) {
0250         s = fakeCString(vObjectUStringZValue(vo));
0251         anEvent->setDescription(QString::fromUtf8(s), Qt::mightBeRichText(QString::fromUtf8(s)));
0252         deleteStr(s);
0253     }
0254 
0255     // summary
0256     if ((vo = isAPropertyOf(vtodo, VCSummaryProp))) {
0257         s = fakeCString(vObjectUStringZValue(vo));
0258         anEvent->setSummary(QString::fromUtf8(s), Qt::mightBeRichText(QString::fromUtf8(s)));
0259         deleteStr(s);
0260     }
0261 
0262     // location
0263     if ((vo = isAPropertyOf(vtodo, VCLocationProp)) != nullptr) {
0264         s = fakeCString(vObjectUStringZValue(vo));
0265         anEvent->setLocation(QString::fromUtf8(s), Qt::mightBeRichText(QString::fromUtf8(s)));
0266         deleteStr(s);
0267     }
0268 
0269     // completed
0270     // was: status
0271     if ((vo = isAPropertyOf(vtodo, VCStatusProp)) != nullptr) {
0272         s = fakeCString(vObjectUStringZValue(vo));
0273         if (s && strcmp(s, "COMPLETED") == 0) {
0274             anEvent->setCompleted(true);
0275         } else {
0276             anEvent->setCompleted(false);
0277         }
0278         deleteStr(s);
0279     } else {
0280         anEvent->setCompleted(false);
0281     }
0282 
0283     // completion date
0284     if ((vo = isAPropertyOf(vtodo, VCCompletedProp)) != nullptr) {
0285         anEvent->setCompleted(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))));
0286         deleteStr(s);
0287     }
0288 
0289     // priority
0290     if ((vo = isAPropertyOf(vtodo, VCPriorityProp))) {
0291         s = fakeCString(vObjectUStringZValue(vo));
0292         if (s) {
0293             anEvent->setPriority(atoi(s));
0294             deleteStr(s);
0295         }
0296     }
0297 
0298     anEvent->setAllDay(false);
0299 
0300     // due date
0301     if ((vo = isAPropertyOf(vtodo, VCDueProp)) != nullptr) {
0302         anEvent->setDtDue(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))));
0303         deleteStr(s);
0304         if (anEvent->dtDue().time().hour() == 0 && anEvent->dtDue().time().minute() == 0 && anEvent->dtDue().time().second() == 0) {
0305             anEvent->setAllDay(true);
0306         }
0307     } else {
0308         anEvent->setDtDue(QDateTime());
0309     }
0310 
0311     // start time
0312     if ((vo = isAPropertyOf(vtodo, VCDTstartProp)) != nullptr) {
0313         anEvent->setDtStart(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))));
0314         deleteStr(s);
0315         if (anEvent->dtStart().time().hour() == 0 && anEvent->dtStart().time().minute() == 0 && anEvent->dtStart().time().second() == 0) {
0316             anEvent->setAllDay(true);
0317         }
0318     } else {
0319         anEvent->setDtStart(QDateTime());
0320     }
0321 
0322     // recurrence stuff
0323     if ((vo = isAPropertyOf(vtodo, VCRRuleProp)) != nullptr) {
0324         uint recurrenceType = Recurrence::rNone;
0325         int recurrenceTypeAbbrLen = 0;
0326 
0327         s = fakeCString(vObjectUStringZValue(vo));
0328         QString tmpStr = QString::fromUtf8(s);
0329         deleteStr(s);
0330         tmpStr = tmpStr.simplified();
0331         const int tmpStrLen = tmpStr.length();
0332         if (tmpStrLen > 0) {
0333             tmpStr = tmpStr.toUpper();
0334             QStringView prefix = QStringView(tmpStr).left(2);
0335 
0336             // first, read the type of the recurrence
0337             recurrenceTypeAbbrLen = 1;
0338             if (tmpStr.at(0) == QLatin1Char('D')) {
0339                 recurrenceType = Recurrence::rDaily;
0340             } else if (tmpStr.at(0) == QLatin1Char('W')) {
0341                 recurrenceType = Recurrence::rWeekly;
0342             } else if (tmpStrLen > 1) {
0343                 recurrenceTypeAbbrLen = 2;
0344                 if (prefix == QLatin1String("MP")) {
0345                     recurrenceType = Recurrence::rMonthlyPos;
0346                 } else if (prefix == QLatin1String("MD")) {
0347                     recurrenceType = Recurrence::rMonthlyDay;
0348                 } else if (prefix == QLatin1String("YM")) {
0349                     recurrenceType = Recurrence::rYearlyMonth;
0350                 } else if (prefix == QLatin1String("YD")) {
0351                     recurrenceType = Recurrence::rYearlyDay;
0352                 }
0353             }
0354         }
0355 
0356         if (recurrenceType != Recurrence::rNone) {
0357             // Immediately after the type is the frequency
0358             int index = tmpStr.indexOf(QLatin1Char(' '));
0359             int last = tmpStr.lastIndexOf(QLatin1Char(' ')) + 1; // find last entry
0360             const int rFreq = QStringView(tmpStr).mid(recurrenceTypeAbbrLen, (index - 1)).toInt();
0361             ++index; // advance to beginning of stuff after freq
0362 
0363             // Read the type-specific settings
0364             switch (recurrenceType) {
0365             case Recurrence::rDaily:
0366                 anEvent->recurrence()->setDaily(rFreq);
0367                 break;
0368 
0369             case Recurrence::rWeekly: {
0370                 QBitArray qba(7);
0371                 QString dayStr;
0372                 if (index == last) {
0373                     // e.g. W1 #0
0374                     qba.setBit(anEvent->dtStart().date().dayOfWeek() - 1);
0375                 } else {
0376                     // e.g. W1 SU #0
0377                     while (index < last) {
0378                         dayStr = tmpStr.mid(index, 3);
0379                         int dayNum = numFromDay(dayStr);
0380                         if (dayNum >= 0) {
0381                             qba.setBit(dayNum);
0382                         }
0383                         index += 3; // advance to next day, or possibly "#"
0384                     }
0385                 }
0386                 anEvent->recurrence()->setWeekly(rFreq, qba);
0387                 break;
0388             }
0389 
0390             case Recurrence::rMonthlyPos: {
0391                 anEvent->recurrence()->setMonthly(rFreq);
0392 
0393                 QBitArray qba(7);
0394                 short tmpPos;
0395                 if (index == last) {
0396                     // e.g. MP1 #0
0397                     tmpPos = anEvent->dtStart().date().day() / 7 + 1;
0398                     if (tmpPos == 5) {
0399                         tmpPos = -1;
0400                     }
0401                     qba.setBit(anEvent->dtStart().date().dayOfWeek() - 1);
0402                     anEvent->recurrence()->addMonthlyPos(tmpPos, qba);
0403                 } else {
0404                     // e.g. MP1 1+ SU #0
0405                     while (index < last) {
0406                         tmpPos = tmpStr.mid(index, 1).toShort();
0407                         index += 1;
0408                         if (tmpStr.mid(index, 1) == QLatin1String("-")) {
0409                             // convert tmpPos to negative
0410                             tmpPos = 0 - tmpPos;
0411                         }
0412                         index += 2; // advance to day(s)
0413                         while (numFromDay(tmpStr.mid(index, 3)) >= 0) {
0414                             int dayNum = numFromDay(tmpStr.mid(index, 3));
0415                             qba.setBit(dayNum);
0416                             index += 3; // advance to next day, or possibly pos or "#"
0417                         }
0418                         anEvent->recurrence()->addMonthlyPos(tmpPos, qba);
0419                         qba.detach();
0420                         qba.fill(false); // clear out
0421                     } // while != "#"
0422                 }
0423                 break;
0424             }
0425 
0426             case Recurrence::rMonthlyDay:
0427                 anEvent->recurrence()->setMonthly(rFreq);
0428                 if (index == last) {
0429                     // e.g. MD1 #0
0430                     short tmpDay = anEvent->dtStart().date().day();
0431                     anEvent->recurrence()->addMonthlyDate(tmpDay);
0432                 } else {
0433                     // e.g. MD1 3 #0
0434                     while (index < last) {
0435                         int index2 = tmpStr.indexOf(QLatin1Char(' '), index);
0436                         if ((tmpStr.mid((index2 - 1), 1) == QLatin1String("-")) || (tmpStr.mid((index2 - 1), 1) == QLatin1String("+"))) {
0437                             index2 = index2 - 1;
0438                         }
0439                         short tmpDay = tmpStr.mid(index, (index2 - index)).toShort();
0440                         index = index2;
0441                         if (tmpStr.mid(index, 1) == QLatin1String("-")) {
0442                             tmpDay = 0 - tmpDay;
0443                         }
0444                         index += 2; // advance the index;
0445                         anEvent->recurrence()->addMonthlyDate(tmpDay);
0446                     } // while != #
0447                 }
0448                 break;
0449 
0450             case Recurrence::rYearlyMonth:
0451                 anEvent->recurrence()->setYearly(rFreq);
0452 
0453                 if (index == last) {
0454                     // e.g. YM1 #0
0455                     short tmpMonth = anEvent->dtStart().date().month();
0456                     anEvent->recurrence()->addYearlyMonth(tmpMonth);
0457                 } else {
0458                     // e.g. YM1 3 #0
0459                     while (index < last) {
0460                         int index2 = tmpStr.indexOf(QLatin1Char(' '), index);
0461                         short tmpMonth = tmpStr.mid(index, (index2 - index)).toShort();
0462                         index = index2 + 1;
0463                         anEvent->recurrence()->addYearlyMonth(tmpMonth);
0464                     } // while != #
0465                 }
0466                 break;
0467 
0468             case Recurrence::rYearlyDay:
0469                 anEvent->recurrence()->setYearly(rFreq);
0470 
0471                 if (index == last) {
0472                     // e.g. YD1 #0
0473                     short tmpDay = anEvent->dtStart().date().dayOfYear();
0474                     anEvent->recurrence()->addYearlyDay(tmpDay);
0475                 } else {
0476                     // e.g. YD1 123 #0
0477                     while (index < last) {
0478                         int index2 = tmpStr.indexOf(QLatin1Char(' '), index);
0479                         short tmpDay = tmpStr.mid(index, (index2 - index)).toShort();
0480                         index = index2 + 1;
0481                         anEvent->recurrence()->addYearlyDay(tmpDay);
0482                     } // while != #
0483                 }
0484                 break;
0485 
0486             default:
0487                 break;
0488             }
0489 
0490             // find the last field, which is either the duration or the end date
0491             index = last;
0492             if (tmpStr.mid(index, 1) == QLatin1String("#")) {
0493                 // Nr of occurrences
0494                 ++index;
0495                 const int rDuration = QStringView(tmpStr).mid(index, tmpStr.length() - index).toInt();
0496                 if (rDuration > 0) {
0497                     anEvent->recurrence()->setDuration(rDuration);
0498                 }
0499             } else if (tmpStr.indexOf(QLatin1Char('T'), index) != -1) {
0500                 QDateTime rEndDate = ISOToQDateTime(tmpStr.mid(index, tmpStr.length() - index));
0501                 anEvent->recurrence()->setEndDateTime(rEndDate);
0502             }
0503         } else {
0504             qCDebug(KCALCORE_LOG) << "we don't understand this type of recurrence!";
0505         } // if known recurrence type
0506     } // repeats
0507 
0508     // recurrence exceptions
0509     if ((vo = isAPropertyOf(vtodo, VCExpDateProp)) != nullptr) {
0510         s = fakeCString(vObjectUStringZValue(vo));
0511         const QStringList exDates = QString::fromUtf8(s).split(QLatin1Char(','));
0512         for (const auto &date : exDates) {
0513             const QDateTime exDate = ISOToQDateTime(date);
0514             if (exDate.time().hour() == 0 && exDate.time().minute() == 0 && exDate.time().second() == 0) {
0515                 anEvent->recurrence()->addExDate(ISOToQDate(date));
0516             } else {
0517                 anEvent->recurrence()->addExDateTime(exDate);
0518             }
0519         }
0520         deleteStr(s);
0521     }
0522 
0523     // alarm stuff
0524     if ((vo = isAPropertyOf(vtodo, VCDAlarmProp))) {
0525         Alarm::Ptr alarm;
0526         VObject *a = isAPropertyOf(vo, VCRunTimeProp);
0527         VObject *b = isAPropertyOf(vo, VCDisplayStringProp);
0528 
0529         if (a || b) {
0530             alarm = anEvent->newAlarm();
0531             if (a) {
0532                 alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a)))));
0533                 deleteStr(s);
0534             }
0535             alarm->setEnabled(true);
0536             if (b) {
0537                 s = fakeCString(vObjectUStringZValue(b));
0538                 alarm->setDisplayAlarm(QString::fromUtf8(s));
0539                 deleteStr(s);
0540             } else {
0541                 alarm->setDisplayAlarm(QString());
0542             }
0543         }
0544     }
0545 
0546     if ((vo = isAPropertyOf(vtodo, VCAAlarmProp))) {
0547         Alarm::Ptr alarm;
0548         VObject *a;
0549         VObject *b;
0550         a = isAPropertyOf(vo, VCRunTimeProp);
0551         b = isAPropertyOf(vo, VCAudioContentProp);
0552 
0553         if (a || b) {
0554             alarm = anEvent->newAlarm();
0555             if (a) {
0556                 alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a)))));
0557                 deleteStr(s);
0558             }
0559             alarm->setEnabled(true);
0560             if (b) {
0561                 s = fakeCString(vObjectUStringZValue(b));
0562                 alarm->setAudioAlarm(QFile::decodeName(s));
0563                 deleteStr(s);
0564             } else {
0565                 alarm->setAudioAlarm(QString());
0566             }
0567         }
0568     }
0569 
0570     if ((vo = isAPropertyOf(vtodo, VCPAlarmProp))) {
0571         Alarm::Ptr alarm;
0572         VObject *a = isAPropertyOf(vo, VCRunTimeProp);
0573         VObject *b = isAPropertyOf(vo, VCProcedureNameProp);
0574 
0575         if (a || b) {
0576             alarm = anEvent->newAlarm();
0577             if (a) {
0578                 alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a)))));
0579                 deleteStr(s);
0580             }
0581             alarm->setEnabled(true);
0582 
0583             if (b) {
0584                 s = fakeCString(vObjectUStringZValue(b));
0585                 alarm->setProcedureAlarm(QFile::decodeName(s));
0586                 deleteStr(s);
0587             } else {
0588                 alarm->setProcedureAlarm(QString());
0589             }
0590         }
0591     }
0592 
0593     // related todo
0594     if ((vo = isAPropertyOf(vtodo, VCRelatedToProp)) != nullptr) {
0595         anEvent->setRelatedTo(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))));
0596         deleteStr(s);
0597         d->mTodosRelate.append(anEvent);
0598     }
0599 
0600     // secrecy
0601     Incidence::Secrecy secrecy = Incidence::SecrecyPublic;
0602     if ((vo = isAPropertyOf(vtodo, VCClassProp)) != nullptr) {
0603         s = fakeCString(vObjectUStringZValue(vo));
0604         if (s && strcmp(s, "PRIVATE") == 0) {
0605             secrecy = Incidence::SecrecyPrivate;
0606         } else if (s && strcmp(s, "CONFIDENTIAL") == 0) {
0607             secrecy = Incidence::SecrecyConfidential;
0608         }
0609         deleteStr(s);
0610     }
0611     anEvent->setSecrecy(secrecy);
0612 
0613     // categories
0614     if ((vo = isAPropertyOf(vtodo, VCCategoriesProp)) != nullptr) {
0615         s = fakeCString(vObjectUStringZValue(vo));
0616         QString categories = QString::fromUtf8(s);
0617         deleteStr(s);
0618         QStringList tmpStrList = categories.split(QLatin1Char(';'));
0619         anEvent->setCategories(tmpStrList);
0620     }
0621 
0622     return anEvent;
0623 }
0624 
0625 Event::Ptr VCalFormat::VEventToEvent(VObject *vevent)
0626 {
0627     Q_D(VCalFormat);
0628     VObject *vo = nullptr;
0629     VObjectIterator voi;
0630     char *s = nullptr;
0631 
0632     Event::Ptr anEvent(new Event);
0633 
0634     // creation date
0635     if ((vo = isAPropertyOf(vevent, VCDCreatedProp)) != nullptr) {
0636         anEvent->setCreated(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))));
0637         deleteStr(s);
0638     }
0639 
0640     // unique id
0641     vo = isAPropertyOf(vevent, VCUniqueStringProp);
0642     // while the UID property is preferred, it is not required.  We'll use the
0643     // default Event UID if none is given.
0644     if (vo) {
0645         anEvent->setUid(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))));
0646         deleteStr(s);
0647     }
0648 
0649     // revision
0650     // again NSCAL doesn't give us much to work with, so we improvise...
0651     anEvent->setRevision(0);
0652     if ((vo = isAPropertyOf(vevent, VCSequenceProp)) != nullptr) {
0653         s = fakeCString(vObjectUStringZValue(vo));
0654         if (s) {
0655             anEvent->setRevision(atoi(s));
0656             deleteStr(s);
0657         }
0658     }
0659 
0660     // last modification date
0661     if ((vo = isAPropertyOf(vevent, VCLastModifiedProp)) != nullptr) {
0662         anEvent->setLastModified(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))));
0663         deleteStr(s);
0664     } else {
0665         anEvent->setLastModified(QDateTime::currentDateTimeUtc());
0666     }
0667 
0668     // organizer
0669     // if our extension property for the event's ORGANIZER exists, add it.
0670     if ((vo = isAPropertyOf(vevent, ICOrganizerProp)) != nullptr) {
0671         // FIXME:  Also use the full name, not just the email address
0672         anEvent->setOrganizer(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))));
0673         deleteStr(s);
0674     } else {
0675         if (d->mCalendar->owner().name() != QLatin1String("Unknown Name")) {
0676             anEvent->setOrganizer(d->mCalendar->owner());
0677         }
0678     }
0679 
0680     // deal with attendees.
0681     initPropIterator(&voi, vevent);
0682     while (moreIteration(&voi)) {
0683         vo = nextVObject(&voi);
0684         if (strcmp(vObjectName(vo), VCAttendeeProp) == 0) {
0685             Attendee a;
0686             VObject *vp = nullptr;
0687             s = fakeCString(vObjectUStringZValue(vo));
0688             QString tmpStr = QString::fromUtf8(s);
0689             deleteStr(s);
0690             tmpStr = tmpStr.simplified();
0691             int emailPos1;
0692             if ((emailPos1 = tmpStr.indexOf(QLatin1Char('<'))) > 0) {
0693                 // both email address and name
0694                 int emailPos2 = tmpStr.lastIndexOf(QLatin1Char('>'));
0695                 a = Attendee(tmpStr.left(emailPos1 - 1), tmpStr.mid(emailPos1 + 1, emailPos2 - (emailPos1 + 1)));
0696             } else if (tmpStr.indexOf(QLatin1Char('@')) > 0) {
0697                 // just an email address
0698                 a = Attendee(QString(), tmpStr);
0699             } else {
0700                 // just a name
0701                 QString email = tmpStr.replace(QLatin1Char(' '), QLatin1Char('.'));
0702                 a = Attendee(tmpStr, email);
0703             }
0704 
0705             // is there an RSVP property?
0706             if ((vp = isAPropertyOf(vo, VCRSVPProp)) != nullptr) {
0707                 a.setRSVP(vObjectStringZValue(vp));
0708             }
0709             // is there a status property?
0710             if ((vp = isAPropertyOf(vo, VCStatusProp)) != nullptr) {
0711                 a.setStatus(readStatus(vObjectStringZValue(vp)));
0712             }
0713             // add the attendee
0714             anEvent->addAttendee(a);
0715         }
0716     }
0717 
0718     // This isn't strictly true.  An event that doesn't have a start time
0719     // or an end time isn't all-day, it has an anchor in time but it doesn't
0720     // "take up" any time.
0721     /*if ((isAPropertyOf(vevent, VCDTstartProp) == 0) ||
0722         (isAPropertyOf(vevent, VCDTendProp) == 0)) {
0723       anEvent->setAllDay(true);
0724       } else {
0725       }*/
0726 
0727     anEvent->setAllDay(false);
0728 
0729     // start time
0730     if ((vo = isAPropertyOf(vevent, VCDTstartProp)) != nullptr) {
0731         anEvent->setDtStart(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))));
0732         deleteStr(s);
0733 
0734         if (anEvent->dtStart().time().hour() == 0 && anEvent->dtStart().time().minute() == 0 && anEvent->dtStart().time().second() == 0) {
0735             anEvent->setAllDay(true);
0736         }
0737     }
0738 
0739     // stop time
0740     if ((vo = isAPropertyOf(vevent, VCDTendProp)) != nullptr) {
0741         anEvent->setDtEnd(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))));
0742         deleteStr(s);
0743 
0744         if (anEvent->dtEnd().time().hour() == 0 && anEvent->dtEnd().time().minute() == 0 && anEvent->dtEnd().time().second() == 0) {
0745             anEvent->setAllDay(true);
0746         }
0747     }
0748 
0749     // at this point, there should be at least a start or end time.
0750     // fix up for events that take up no time but have a time associated
0751     if (!isAPropertyOf(vevent, VCDTstartProp)) {
0752         anEvent->setDtStart(anEvent->dtEnd());
0753     }
0754     if (!isAPropertyOf(vevent, VCDTendProp)) {
0755         anEvent->setDtEnd(anEvent->dtStart());
0756     }
0757 
0758     ///////////////////////////////////////////////////////////////////////////
0759 
0760     // recurrence stuff
0761     if ((vo = isAPropertyOf(vevent, VCRRuleProp)) != nullptr) {
0762         uint recurrenceType = Recurrence::rNone;
0763         int recurrenceTypeAbbrLen = 0;
0764 
0765         QString tmpStr = (QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))));
0766         deleteStr(s);
0767         tmpStr = tmpStr.simplified();
0768         const int tmpStrLen = tmpStr.length();
0769         if (tmpStrLen > 0) {
0770             tmpStr = tmpStr.toUpper();
0771             const QStringView prefix(tmpStr.left(2));
0772 
0773             // first, read the type of the recurrence
0774             recurrenceTypeAbbrLen = 1;
0775             if (tmpStr.at(0) == QLatin1Char('D')) {
0776                 recurrenceType = Recurrence::rDaily;
0777             } else if (tmpStr.at(0) == QLatin1Char('W')) {
0778                 recurrenceType = Recurrence::rWeekly;
0779             } else if (tmpStrLen > 1) {
0780                 recurrenceTypeAbbrLen = 2;
0781                 if (prefix == QLatin1String("MP")) {
0782                     recurrenceType = Recurrence::rMonthlyPos;
0783                 } else if (prefix == QLatin1String("MD")) {
0784                     recurrenceType = Recurrence::rMonthlyDay;
0785                 } else if (prefix == QLatin1String("YM")) {
0786                     recurrenceType = Recurrence::rYearlyMonth;
0787                 } else if (prefix == QLatin1String("YD")) {
0788                     recurrenceType = Recurrence::rYearlyDay;
0789                 }
0790             }
0791         }
0792 
0793         if (recurrenceType != Recurrence::rNone) {
0794             // Immediately after the type is the frequency
0795             int index = tmpStr.indexOf(QLatin1Char(' '));
0796             int last = tmpStr.lastIndexOf(QLatin1Char(' ')) + 1; // find last entry
0797             const int rFreq = QStringView(tmpStr).mid(recurrenceTypeAbbrLen, (index - 1)).toInt();
0798             ++index; // advance to beginning of stuff after freq
0799 
0800             // Read the type-specific settings
0801             switch (recurrenceType) {
0802             case Recurrence::rDaily:
0803                 anEvent->recurrence()->setDaily(rFreq);
0804                 break;
0805 
0806             case Recurrence::rWeekly: {
0807                 QBitArray qba(7);
0808                 QString dayStr;
0809                 if (index == last) {
0810                     // e.g. W1 #0
0811                     qba.setBit(anEvent->dtStart().date().dayOfWeek() - 1);
0812                 } else {
0813                     // e.g. W1 SU #0
0814                     while (index < last) {
0815                         dayStr = tmpStr.mid(index, 3);
0816                         int dayNum = numFromDay(dayStr);
0817                         if (dayNum >= 0) {
0818                             qba.setBit(dayNum);
0819                         }
0820                         index += 3; // advance to next day, or possibly "#"
0821                     }
0822                 }
0823                 anEvent->recurrence()->setWeekly(rFreq, qba);
0824                 break;
0825             }
0826 
0827             case Recurrence::rMonthlyPos: {
0828                 anEvent->recurrence()->setMonthly(rFreq);
0829 
0830                 QBitArray qba(7);
0831                 short tmpPos;
0832                 if (index == last) {
0833                     // e.g. MP1 #0
0834                     tmpPos = anEvent->dtStart().date().day() / 7 + 1;
0835                     if (tmpPos == 5) {
0836                         tmpPos = -1;
0837                     }
0838                     qba.setBit(anEvent->dtStart().date().dayOfWeek() - 1);
0839                     anEvent->recurrence()->addMonthlyPos(tmpPos, qba);
0840                 } else {
0841                     // e.g. MP1 1+ SU #0
0842                     while (index < last) {
0843                         tmpPos = tmpStr.mid(index, 1).toShort();
0844                         index += 1;
0845                         if (tmpStr.mid(index, 1) == QLatin1String("-")) {
0846                             // convert tmpPos to negative
0847                             tmpPos = 0 - tmpPos;
0848                         }
0849                         index += 2; // advance to day(s)
0850                         while (numFromDay(tmpStr.mid(index, 3)) >= 0) {
0851                             int dayNum = numFromDay(tmpStr.mid(index, 3));
0852                             qba.setBit(dayNum);
0853                             index += 3; // advance to next day, or possibly pos or "#"
0854                         }
0855                         anEvent->recurrence()->addMonthlyPos(tmpPos, qba);
0856                         qba.detach();
0857                         qba.fill(false); // clear out
0858                     } // while != "#"
0859                 }
0860                 break;
0861             }
0862 
0863             case Recurrence::rMonthlyDay:
0864                 anEvent->recurrence()->setMonthly(rFreq);
0865                 if (index == last) {
0866                     // e.g. MD1 #0
0867                     short tmpDay = anEvent->dtStart().date().day();
0868                     anEvent->recurrence()->addMonthlyDate(tmpDay);
0869                 } else {
0870                     // e.g. MD1 3 #0
0871                     while (index < last) {
0872                         int index2 = tmpStr.indexOf(QLatin1Char(' '), index);
0873                         if ((tmpStr.mid((index2 - 1), 1) == QLatin1String("-")) || (tmpStr.mid((index2 - 1), 1) == QLatin1String("+"))) {
0874                             index2 = index2 - 1;
0875                         }
0876                         short tmpDay = tmpStr.mid(index, (index2 - index)).toShort();
0877                         index = index2;
0878                         if (tmpStr.mid(index, 1) == QLatin1String("-")) {
0879                             tmpDay = 0 - tmpDay;
0880                         }
0881                         index += 2; // advance the index;
0882                         anEvent->recurrence()->addMonthlyDate(tmpDay);
0883                     } // while != #
0884                 }
0885                 break;
0886 
0887             case Recurrence::rYearlyMonth:
0888                 anEvent->recurrence()->setYearly(rFreq);
0889 
0890                 if (index == last) {
0891                     // e.g. YM1 #0
0892                     short tmpMonth = anEvent->dtStart().date().month();
0893                     anEvent->recurrence()->addYearlyMonth(tmpMonth);
0894                 } else {
0895                     // e.g. YM1 3 #0
0896                     while (index < last) {
0897                         int index2 = tmpStr.indexOf(QLatin1Char(' '), index);
0898                         short tmpMonth = tmpStr.mid(index, (index2 - index)).toShort();
0899                         index = index2 + 1;
0900                         anEvent->recurrence()->addYearlyMonth(tmpMonth);
0901                     } // while != #
0902                 }
0903                 break;
0904 
0905             case Recurrence::rYearlyDay:
0906                 anEvent->recurrence()->setYearly(rFreq);
0907 
0908                 if (index == last) {
0909                     // e.g. YD1 #0
0910                     const int tmpDay = anEvent->dtStart().date().dayOfYear();
0911                     anEvent->recurrence()->addYearlyDay(tmpDay);
0912                 } else {
0913                     // e.g. YD1 123 #0
0914                     while (index < last) {
0915                         int index2 = tmpStr.indexOf(QLatin1Char(' '), index);
0916                         short tmpDay = tmpStr.mid(index, (index2 - index)).toShort();
0917                         index = index2 + 1;
0918                         anEvent->recurrence()->addYearlyDay(tmpDay);
0919                     } // while != #
0920                 }
0921                 break;
0922 
0923             default:
0924                 break;
0925             }
0926 
0927             // find the last field, which is either the duration or the end date
0928             index = last;
0929             if (tmpStr.mid(index, 1) == QLatin1String("#")) {
0930                 // Nr of occurrences
0931                 ++index;
0932                 const int rDuration = QStringView(tmpStr).mid(index, tmpStr.length() - index).toInt();
0933                 if (rDuration > 0) {
0934                     anEvent->recurrence()->setDuration(rDuration);
0935                 }
0936             } else if (tmpStr.indexOf(QLatin1Char('T'), index) != -1) {
0937                 QDateTime rEndDate = ISOToQDateTime(tmpStr.mid(index, tmpStr.length() - index));
0938                 anEvent->recurrence()->setEndDateTime(rEndDate);
0939             }
0940             // anEvent->recurrence()->dump();
0941 
0942         } else {
0943             qCDebug(KCALCORE_LOG) << "we don't understand this type of recurrence!";
0944         } // if known recurrence type
0945     } // repeats
0946 
0947     // recurrence exceptions
0948     if ((vo = isAPropertyOf(vevent, VCExpDateProp)) != nullptr) {
0949         s = fakeCString(vObjectUStringZValue(vo));
0950         const QStringList exDates = QString::fromUtf8(s).split(QLatin1Char(','));
0951         for (const auto &date : exDates) {
0952             const QDateTime exDate = ISOToQDateTime(date);
0953             if (exDate.time().hour() == 0 && exDate.time().minute() == 0 && exDate.time().second() == 0) {
0954                 anEvent->recurrence()->addExDate(ISOToQDate(date));
0955             } else {
0956                 anEvent->recurrence()->addExDateTime(exDate);
0957             }
0958         }
0959         deleteStr(s);
0960     }
0961 
0962     // summary
0963     if ((vo = isAPropertyOf(vevent, VCSummaryProp))) {
0964         s = fakeCString(vObjectUStringZValue(vo));
0965         anEvent->setSummary(QString::fromUtf8(s), Qt::mightBeRichText(QString::fromUtf8(s)));
0966         deleteStr(s);
0967     }
0968 
0969     // description
0970     if ((vo = isAPropertyOf(vevent, VCDescriptionProp)) != nullptr) {
0971         s = fakeCString(vObjectUStringZValue(vo));
0972         bool isRich = Qt::mightBeRichText(QString::fromUtf8(s));
0973         if (!anEvent->description().isEmpty()) {
0974             anEvent->setDescription(anEvent->description() + QLatin1Char('\n') + QString::fromUtf8(s), isRich);
0975         } else {
0976             anEvent->setDescription(QString::fromUtf8(s), isRich);
0977         }
0978         deleteStr(s);
0979     }
0980 
0981     // location
0982     if ((vo = isAPropertyOf(vevent, VCLocationProp)) != nullptr) {
0983         s = fakeCString(vObjectUStringZValue(vo));
0984         anEvent->setLocation(QString::fromUtf8(s), Qt::mightBeRichText(QString::fromUtf8(s)));
0985         deleteStr(s);
0986     }
0987 
0988     // some stupid vCal exporters ignore the standard and use Description
0989     // instead of Summary for the default field.  Correct for this.
0990     if (anEvent->summary().isEmpty() && !(anEvent->description().isEmpty())) {
0991         QString tmpStr = anEvent->description().simplified();
0992         anEvent->setDescription(QString());
0993         anEvent->setSummary(tmpStr);
0994     }
0995 
0996 #if 0
0997     // status
0998     if ((vo = isAPropertyOf(vevent, VCStatusProp)) != 0) {
0999         QString tmpStr(s = fakeCString(vObjectUStringZValue(vo)));
1000         deleteStr(s);
1001 // TODO: Define Event status
1002 //    anEvent->setStatus( tmpStr );
1003     } else {
1004 //    anEvent->setStatus( "NEEDS ACTION" );
1005     }
1006 #endif
1007 
1008     // secrecy
1009     Incidence::Secrecy secrecy = Incidence::SecrecyPublic;
1010     if ((vo = isAPropertyOf(vevent, VCClassProp)) != nullptr) {
1011         s = fakeCString(vObjectUStringZValue(vo));
1012         if (s && strcmp(s, "PRIVATE") == 0) {
1013             secrecy = Incidence::SecrecyPrivate;
1014         } else if (s && strcmp(s, "CONFIDENTIAL") == 0) {
1015             secrecy = Incidence::SecrecyConfidential;
1016         }
1017         deleteStr(s);
1018     }
1019     anEvent->setSecrecy(secrecy);
1020 
1021     // categories
1022     if ((vo = isAPropertyOf(vevent, VCCategoriesProp)) != nullptr) {
1023         s = fakeCString(vObjectUStringZValue(vo));
1024         QString categories = QString::fromUtf8(s);
1025         deleteStr(s);
1026         QStringList tmpStrList = categories.split(QLatin1Char(','));
1027         anEvent->setCategories(tmpStrList);
1028     }
1029 
1030     // attachments
1031     initPropIterator(&voi, vevent);
1032     while (moreIteration(&voi)) {
1033         vo = nextVObject(&voi);
1034         if (strcmp(vObjectName(vo), VCAttachProp) == 0) {
1035             s = fakeCString(vObjectUStringZValue(vo));
1036             anEvent->addAttachment(Attachment(QString::fromUtf8(s)));
1037             deleteStr(s);
1038         }
1039     }
1040 
1041     // resources
1042     if ((vo = isAPropertyOf(vevent, VCResourcesProp)) != nullptr) {
1043         QString resources = (QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))));
1044         deleteStr(s);
1045         QStringList tmpStrList = resources.split(QLatin1Char(';'));
1046         anEvent->setResources(tmpStrList);
1047     }
1048 
1049     // alarm stuff
1050     if ((vo = isAPropertyOf(vevent, VCDAlarmProp))) {
1051         Alarm::Ptr alarm;
1052         VObject *a = isAPropertyOf(vo, VCRunTimeProp);
1053         VObject *b = isAPropertyOf(vo, VCDisplayStringProp);
1054 
1055         if (a || b) {
1056             alarm = anEvent->newAlarm();
1057             if (a) {
1058                 alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a)))));
1059                 deleteStr(s);
1060             }
1061             alarm->setEnabled(true);
1062 
1063             if (b) {
1064                 s = fakeCString(vObjectUStringZValue(b));
1065                 alarm->setDisplayAlarm(QString::fromUtf8(s));
1066                 deleteStr(s);
1067             } else {
1068                 alarm->setDisplayAlarm(QString());
1069             }
1070         }
1071     }
1072 
1073     if ((vo = isAPropertyOf(vevent, VCAAlarmProp))) {
1074         Alarm::Ptr alarm;
1075         VObject *a;
1076         VObject *b;
1077         a = isAPropertyOf(vo, VCRunTimeProp);
1078         b = isAPropertyOf(vo, VCAudioContentProp);
1079 
1080         if (a || b) {
1081             alarm = anEvent->newAlarm();
1082             if (a) {
1083                 alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a)))));
1084                 deleteStr(s);
1085             }
1086             alarm->setEnabled(true);
1087 
1088             if (b) {
1089                 s = fakeCString(vObjectUStringZValue(b));
1090                 alarm->setAudioAlarm(QFile::decodeName(s));
1091                 deleteStr(s);
1092             } else {
1093                 alarm->setAudioAlarm(QString());
1094             }
1095         }
1096     }
1097 
1098     if ((vo = isAPropertyOf(vevent, VCPAlarmProp))) {
1099         Alarm::Ptr alarm;
1100         VObject *a;
1101         VObject *b;
1102         a = isAPropertyOf(vo, VCRunTimeProp);
1103         b = isAPropertyOf(vo, VCProcedureNameProp);
1104 
1105         if (a || b) {
1106             alarm = anEvent->newAlarm();
1107             if (a) {
1108                 alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a)))));
1109                 deleteStr(s);
1110             }
1111             alarm->setEnabled(true);
1112 
1113             if (b) {
1114                 s = fakeCString(vObjectUStringZValue(b));
1115                 alarm->setProcedureAlarm(QFile::decodeName(s));
1116                 deleteStr(s);
1117             } else {
1118                 alarm->setProcedureAlarm(QString());
1119             }
1120         }
1121     }
1122 
1123     // priority
1124     if ((vo = isAPropertyOf(vevent, VCPriorityProp))) {
1125         s = fakeCString(vObjectUStringZValue(vo));
1126         if (s) {
1127             anEvent->setPriority(atoi(s));
1128             deleteStr(s);
1129         }
1130     }
1131 
1132     // transparency
1133     if ((vo = isAPropertyOf(vevent, VCTranspProp)) != nullptr) {
1134         s = fakeCString(vObjectUStringZValue(vo));
1135         if (s) {
1136             int i = atoi(s);
1137             anEvent->setTransparency(i == 1 ? Event::Transparent : Event::Opaque);
1138             deleteStr(s);
1139         }
1140     }
1141 
1142     // related event
1143     if ((vo = isAPropertyOf(vevent, VCRelatedToProp)) != nullptr) {
1144         anEvent->setRelatedTo(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))));
1145         deleteStr(s);
1146         d->mEventsRelate.append(anEvent);
1147     }
1148 
1149     /* Rest of the custom properties */
1150     readCustomProperties(vevent, anEvent);
1151 
1152     return anEvent;
1153 }
1154 
1155 QString VCalFormat::parseTZ(const QByteArray &timezone) const
1156 {
1157     // qCDebug(KCALCORE_LOG) << timezone;
1158     QString pZone = QString::fromUtf8(timezone.mid(timezone.indexOf("TZID:VCAL") + 9));
1159     return pZone.mid(0, pZone.indexOf(QLatin1Char('\n')));
1160 }
1161 
1162 QString VCalFormat::parseDst(QByteArray &timezone) const
1163 {
1164     if (!timezone.contains("BEGIN:DAYLIGHT")) {
1165         return QString();
1166     }
1167 
1168     timezone = timezone.mid(timezone.indexOf("BEGIN:DAYLIGHT"));
1169     timezone = timezone.mid(timezone.indexOf("TZNAME:") + 7);
1170     QString sStart = QString::fromUtf8(timezone.mid(0, (timezone.indexOf("COMMENT:"))));
1171     sStart.chop(2);
1172     timezone = timezone.mid(timezone.indexOf("TZOFFSETTO:") + 11);
1173     QString sOffset = QString::fromUtf8(timezone.mid(0, (timezone.indexOf("DTSTART:"))));
1174     sOffset.chop(2);
1175     sOffset.insert(3, QLatin1Char(':'));
1176     timezone = timezone.mid(timezone.indexOf("TZNAME:") + 7);
1177     QString sEnd = QString::fromUtf8(timezone.mid(0, (timezone.indexOf("COMMENT:"))));
1178     sEnd.chop(2);
1179 
1180     return QStringLiteral("TRUE;") + sOffset + QLatin1Char(';') + sStart + QLatin1Char(';') + sEnd + QLatin1String(";;");
1181 }
1182 
1183 QString VCalFormat::qDateToISO(const QDate &qd)
1184 {
1185     if (!qd.isValid()) {
1186         return QString();
1187     }
1188 
1189     return QString::asprintf("%.2d%.2d%.2d", qd.year(), qd.month(), qd.day());
1190 }
1191 
1192 QString VCalFormat::qDateTimeToISO(const QDateTime &dt, bool zulu)
1193 {
1194     Q_D(VCalFormat);
1195     if (!dt.isValid()) {
1196         return QString();
1197     }
1198 
1199     QDateTime tmpDT;
1200     if (zulu) {
1201         tmpDT = dt.toUTC();
1202     } else {
1203         tmpDT = dt.toTimeZone(d->mCalendar->timeZone());
1204     }
1205     QString tmpStr = QString::asprintf("%.2d%.2d%.2dT%.2d%.2d%.2d",
1206                                        tmpDT.date().year(),
1207                                        tmpDT.date().month(),
1208                                        tmpDT.date().day(),
1209                                        tmpDT.time().hour(),
1210                                        tmpDT.time().minute(),
1211                                        tmpDT.time().second());
1212     if (zulu || dt.timeZone() == QTimeZone::utc()) {
1213         tmpStr += QLatin1Char('Z');
1214     }
1215     return tmpStr;
1216 }
1217 
1218 QDateTime VCalFormat::ISOToQDateTime(const QString &dtStr)
1219 {
1220     Q_D(VCalFormat);
1221     auto noAllocString = QStringView{dtStr};
1222 
1223     int year = noAllocString.left(4).toInt();
1224     int month = noAllocString.mid(4, 2).toInt();
1225     int day = noAllocString.mid(6, 2).toInt();
1226     int hour = noAllocString.mid(9, 2).toInt();
1227     int minute = noAllocString.mid(11, 2).toInt();
1228     int second = noAllocString.mid(13, 2).toInt();
1229 
1230     QDate tmpDate;
1231     tmpDate.setDate(year, month, day);
1232     QTime tmpTime;
1233     tmpTime.setHMS(hour, minute, second);
1234 
1235     if (tmpDate.isValid() && tmpTime.isValid()) {
1236         // correct for GMT if string is in Zulu format
1237         if (dtStr.at(dtStr.length() - 1) == QLatin1Char('Z')) {
1238             return QDateTime(tmpDate, tmpTime, QTimeZone::UTC);
1239         } else {
1240             return QDateTime(tmpDate, tmpTime, d->mCalendar->timeZone());
1241         }
1242     }
1243 
1244     return {};
1245 }
1246 
1247 QDate VCalFormat::ISOToQDate(const QString &dateStr)
1248 {
1249     auto noAllocString = QStringView{dateStr};
1250 
1251     const int year = noAllocString.left(4).toInt();
1252     const int month = noAllocString.mid(4, 2).toInt();
1253     const int day = noAllocString.mid(6, 2).toInt();
1254 
1255     return QDate(year, month, day);
1256 }
1257 
1258 bool VCalFormat::parseTZOffsetISO8601(const QString &s, int &result)
1259 {
1260     // ISO8601 format(s):
1261     // +- hh : mm
1262     // +- hh mm
1263     // +- hh
1264 
1265     // We also accept broken one without +
1266     int mod = 1;
1267     int v = 0;
1268     const QString str = s.trimmed();
1269     int ofs = 0;
1270     result = 0;
1271 
1272     // Check for end
1273     if (str.size() <= ofs) {
1274         return false;
1275     }
1276     if (str[ofs] == QLatin1Char('-')) {
1277         mod = -1;
1278         ofs++;
1279     } else if (str[ofs] == QLatin1Char('+')) {
1280         ofs++;
1281     }
1282     if (str.size() <= ofs) {
1283         return false;
1284     }
1285 
1286     // Make sure next two values are numbers
1287     bool ok;
1288 
1289     if (str.size() < (ofs + 2)) {
1290         return false;
1291     }
1292 
1293     v = QStringView(str).mid(ofs, 2).toInt(&ok) * 60;
1294     if (!ok) {
1295         return false;
1296     }
1297     ofs += 2;
1298 
1299     if (str.size() > ofs) {
1300         if (str[ofs] == QLatin1Char(':')) {
1301             ofs++;
1302         }
1303         if (str.size() > ofs) {
1304             if (str.size() < (ofs + 2)) {
1305                 return false;
1306             }
1307             v += QStringView(str).mid(ofs, 2).toInt(&ok);
1308             if (!ok) {
1309                 return false;
1310             }
1311         }
1312     }
1313     result = v * mod * 60;
1314     return true;
1315 }
1316 
1317 // take a raw vcalendar (i.e. from a file on disk, clipboard, etc. etc.
1318 // and break it down from it's tree-like format into the dictionary format
1319 // that is used internally in the VCalFormat.
1320 void VCalFormat::populate(VObject *vcal)
1321 {
1322     Q_D(VCalFormat);
1323     // this function will populate the caldict dictionary and other event
1324     // lists. It turns vevents into Events and then inserts them.
1325 
1326     VObjectIterator i;
1327     VObject *curVO;
1328     Event::Ptr anEvent;
1329     bool hasTimeZone = false; // The calendar came with a TZ and not UTC
1330     QTimeZone previousZone; // If we add a new TZ we should leave the spec as it was before
1331 
1332     if ((curVO = isAPropertyOf(vcal, ICMethodProp)) != nullptr) {
1333         char *methodType = fakeCString(vObjectUStringZValue(curVO));
1334         // qCDebug(KCALCORE_LOG) << "This calendar is an iTIP transaction of type '" << methodType << "'";
1335         deleteStr(methodType);
1336     }
1337 
1338     // warn the user that we might have trouble reading non-known calendar.
1339     if ((curVO = isAPropertyOf(vcal, VCProdIdProp)) != nullptr) {
1340         char *s = fakeCString(vObjectUStringZValue(curVO));
1341         if (!s || strcmp(productId().toUtf8().constData(), s) != 0) {
1342             qCDebug(KCALCORE_LOG) << "This vCalendar file was not created by KOrganizer or"
1343                                   << "any other product we support. Loading anyway...";
1344         }
1345         setLoadedProductId(QString::fromUtf8(s));
1346         deleteStr(s);
1347     }
1348 
1349     // warn the user we might have trouble reading this unknown version.
1350     if ((curVO = isAPropertyOf(vcal, VCVersionProp)) != nullptr) {
1351         char *s = fakeCString(vObjectUStringZValue(curVO));
1352         if (!s || strcmp(_VCAL_VERSION, s) != 0) {
1353             qCDebug(KCALCORE_LOG) << "This vCalendar file has version" << s << "We only support" << _VCAL_VERSION;
1354         }
1355         deleteStr(s);
1356     }
1357 
1358     // set the time zone (this is a property of the view, so just discard!)
1359     if ((curVO = isAPropertyOf(vcal, VCTimeZoneProp)) != nullptr) {
1360         char *s = fakeCString(vObjectUStringZValue(curVO));
1361         QString ts = QString::fromUtf8(s);
1362         QString name = QLatin1String("VCAL") + ts;
1363         deleteStr(s);
1364 
1365         // TODO: While using the timezone-offset + vcal as timezone is is
1366         // most likely unique, we should REALLY actually create something
1367         // like vcal-tzoffset-daylightoffsets, or better yet,
1368         // vcal-hash<the former>
1369 
1370         QStringList tzList;
1371         QString tz;
1372         int utcOffset;
1373         if (parseTZOffsetISO8601(ts, utcOffset)) {
1374             // qCDebug(KCALCORE_LOG) << "got standard offset" << ts << utcOffset;
1375             // standard from tz
1376             // starting date for now 01011900
1377             QDateTime dt = QDateTime(QDateTime(QDate(1900, 1, 1), QTime(0, 0, 0)));
1378             tz = QStringLiteral("STD;%1;false;%2").arg(QString::number(utcOffset), dt.toString());
1379             tzList.append(tz);
1380 
1381             // go through all the daylight tags
1382             initPropIterator(&i, vcal);
1383             while (moreIteration(&i)) {
1384                 curVO = nextVObject(&i);
1385                 if (strcmp(vObjectName(curVO), VCDayLightProp) == 0) {
1386                     char *s = fakeCString(vObjectUStringZValue(curVO));
1387                     QString dst = QLatin1String(s);
1388                     QStringList argl = dst.split(QLatin1Char(','));
1389                     deleteStr(s);
1390 
1391                     // Too short -> not interesting
1392                     if (argl.size() < 4) {
1393                         continue;
1394                     }
1395 
1396                     // We don't care about the non-DST periods
1397                     if (argl[0] != QLatin1String("TRUE")) {
1398                         continue;
1399                     }
1400 
1401                     int utcOffsetDst;
1402                     if (parseTZOffsetISO8601(argl[1], utcOffsetDst)) {
1403                         // qCDebug(KCALCORE_LOG) << "got DST offset" << argl[1] << utcOffsetDst;
1404                         // standard
1405                         QString strEndDate = argl[3];
1406                         QDateTime endDate = ISOToQDateTime(strEndDate);
1407                         // daylight
1408                         QString strStartDate = argl[2];
1409                         QDateTime startDate = ISOToQDateTime(strStartDate);
1410 
1411                         QString strRealEndDate = strEndDate;
1412                         QString strRealStartDate = strStartDate;
1413                         QDateTime realEndDate = endDate;
1414                         QDateTime realStartDate = startDate;
1415                         // if we get dates for some reason in wrong order, earlier is used for dst
1416                         if (endDate < startDate) {
1417                             strRealEndDate = strStartDate;
1418                             strRealStartDate = strEndDate;
1419                             realEndDate = startDate;
1420                             realStartDate = endDate;
1421                         }
1422                         tz = QStringLiteral("%1;%2;false;%3").arg(strRealEndDate, QString::number(utcOffset), realEndDate.toString());
1423                         tzList.append(tz);
1424 
1425                         tz = QStringLiteral("%1;%2;true;%3").arg(strRealStartDate, QString::number(utcOffsetDst), realStartDate.toString());
1426                         tzList.append(tz);
1427                     } else {
1428                         qCDebug(KCALCORE_LOG) << "unable to parse dst" << argl[1];
1429                     }
1430                 }
1431             }
1432             if (!QTimeZone::isTimeZoneIdAvailable(name.toLatin1())) {
1433                 qCDebug(KCALCORE_LOG) << "zone is not valid, parsing error" << tzList;
1434             } else {
1435                 previousZone = d->mCalendar->timeZone();
1436                 d->mCalendar->setTimeZoneId(name.toUtf8());
1437                 hasTimeZone = true;
1438             }
1439         } else {
1440             qCDebug(KCALCORE_LOG) << "unable to parse tzoffset" << ts;
1441         }
1442     }
1443 
1444     // Store all events with a relatedTo property in a list for post-processing
1445     d->mEventsRelate.clear();
1446     d->mTodosRelate.clear();
1447 
1448     initPropIterator(&i, vcal);
1449 
1450     // go through all the vobjects in the vcal
1451     while (moreIteration(&i)) {
1452         curVO = nextVObject(&i);
1453 
1454         /************************************************************************/
1455 
1456         // now, check to see that the object is an event or todo.
1457         if (strcmp(vObjectName(curVO), VCEventProp) == 0) {
1458             if (!isAPropertyOf(curVO, VCDTstartProp) && !isAPropertyOf(curVO, VCDTendProp)) {
1459                 qCDebug(KCALCORE_LOG) << "found a VEvent with no DTSTART and no DTEND! Skipping...";
1460                 goto SKIP;
1461             }
1462 
1463             anEvent = VEventToEvent(curVO);
1464             if (anEvent) {
1465                 if (hasTimeZone && !anEvent->allDay() && anEvent->dtStart().timeZone() == QTimeZone::utc()) {
1466                     // This sounds stupid but is how others are doing it, so here
1467                     // we go. If there is a TZ in the VCALENDAR even if the dtStart
1468                     // and dtend are in UTC, clients interpret it using also the TZ defined
1469                     // in the Calendar. I know it sounds braindead but oh well
1470                     int utcOffSet = anEvent->dtStart().offsetFromUtc();
1471                     QDateTime dtStart(anEvent->dtStart().addSecs(utcOffSet));
1472                     dtStart.setTimeZone(d->mCalendar->timeZone());
1473                     QDateTime dtEnd(anEvent->dtEnd().addSecs(utcOffSet));
1474                     dtEnd.setTimeZone(d->mCalendar->timeZone());
1475                     anEvent->setDtStart(dtStart);
1476                     anEvent->setDtEnd(dtEnd);
1477                 }
1478                 Event::Ptr old =
1479                     !anEvent->hasRecurrenceId() ? d->mCalendar->event(anEvent->uid()) : d->mCalendar->event(anEvent->uid(), anEvent->recurrenceId());
1480 
1481                 if (old) {
1482                     if (anEvent->revision() > old->revision()) {
1483                         d->mCalendar->deleteEvent(old); // move old to deleted
1484                         removeAllVCal(d->mEventsRelate, old);
1485                         d->mCalendar->addEvent(anEvent); // and replace it with this one
1486                     }
1487                 } else {
1488                     d->mCalendar->addEvent(anEvent); // just add this one
1489                 }
1490             }
1491         } else if (strcmp(vObjectName(curVO), VCTodoProp) == 0) {
1492             Todo::Ptr aTodo = VTodoToEvent(curVO);
1493             if (aTodo) {
1494                 if (hasTimeZone && !aTodo->allDay() && aTodo->dtStart().timeZone() == QTimeZone::utc()) {
1495                     // This sounds stupid but is how others are doing it, so here
1496                     // we go. If there is a TZ in the VCALENDAR even if the dtStart
1497                     // and dtend are in UTC, clients interpret it using also the TZ defined
1498                     // in the Calendar. I know it sounds braindead but oh well
1499                     int utcOffSet = aTodo->dtStart().offsetFromUtc();
1500                     QDateTime dtStart(aTodo->dtStart().addSecs(utcOffSet));
1501                     dtStart.setTimeZone(d->mCalendar->timeZone());
1502                     aTodo->setDtStart(dtStart);
1503                     if (aTodo->hasDueDate()) {
1504                         QDateTime dtDue(aTodo->dtDue().addSecs(utcOffSet));
1505                         dtDue.setTimeZone(d->mCalendar->timeZone());
1506                         aTodo->setDtDue(dtDue);
1507                     }
1508                 }
1509                 Todo::Ptr old = !aTodo->hasRecurrenceId() ? d->mCalendar->todo(aTodo->uid()) : d->mCalendar->todo(aTodo->uid(), aTodo->recurrenceId());
1510                 if (old) {
1511                     if (aTodo->revision() > old->revision()) {
1512                         d->mCalendar->deleteTodo(old); // move old to deleted
1513                         removeAllVCal(d->mTodosRelate, old);
1514                         d->mCalendar->addTodo(aTodo); // and replace it with this one
1515                     }
1516                 } else {
1517                     d->mCalendar->addTodo(aTodo); // just add this one
1518                 }
1519             }
1520         } else if ((strcmp(vObjectName(curVO), VCVersionProp) == 0) || (strcmp(vObjectName(curVO), VCProdIdProp) == 0)
1521                    || (strcmp(vObjectName(curVO), VCTimeZoneProp) == 0)) {
1522             // do nothing, we know these properties and we want to skip them.
1523             // we have either already processed them or are ignoring them.
1524             ;
1525         } else if (strcmp(vObjectName(curVO), VCDayLightProp) == 0) {
1526             // do nothing daylights are already processed
1527             ;
1528         } else {
1529             qCDebug(KCALCORE_LOG) << "Ignoring unknown vObject \"" << vObjectName(curVO) << "\"";
1530         }
1531     SKIP:;
1532     } // while
1533 
1534     // Post-Process list of events with relations, put Event objects in relation
1535     for (const auto &eventPtr : std::as_const(d->mEventsRelate)) {
1536         eventPtr->setRelatedTo(eventPtr->relatedTo());
1537     }
1538 
1539     for (const auto &todoPtr : std::as_const(d->mTodosRelate)) {
1540         todoPtr->setRelatedTo(todoPtr->relatedTo());
1541     }
1542 
1543     // Now lets put the TZ back as it was if we have changed it.
1544     if (hasTimeZone) {
1545         d->mCalendar->setTimeZone(previousZone);
1546     }
1547 }
1548 
1549 int VCalFormat::numFromDay(const QString &day)
1550 {
1551     if (day == QLatin1String("MO ")) {
1552         return 0;
1553     }
1554     if (day == QLatin1String("TU ")) {
1555         return 1;
1556     }
1557     if (day == QLatin1String("WE ")) {
1558         return 2;
1559     }
1560     if (day == QLatin1String("TH ")) {
1561         return 3;
1562     }
1563     if (day == QLatin1String("FR ")) {
1564         return 4;
1565     }
1566     if (day == QLatin1String("SA ")) {
1567         return 5;
1568     }
1569     if (day == QLatin1String("SU ")) {
1570         return 6;
1571     }
1572 
1573     return -1; // something bad happened. :)
1574 }
1575 
1576 Attendee::PartStat VCalFormat::readStatus(const char *s) const
1577 {
1578     QString statStr = QString::fromUtf8(s);
1579     statStr = statStr.toUpper();
1580     Attendee::PartStat status;
1581 
1582     if (statStr == QLatin1String("X-ACTION")) {
1583         status = Attendee::NeedsAction;
1584     } else if (statStr == QLatin1String("NEEDS ACTION")) {
1585         status = Attendee::NeedsAction;
1586     } else if (statStr == QLatin1String("ACCEPTED")) {
1587         status = Attendee::Accepted;
1588     } else if (statStr == QLatin1String("SENT")) {
1589         status = Attendee::NeedsAction;
1590     } else if (statStr == QLatin1String("TENTATIVE")) {
1591         status = Attendee::Tentative;
1592     } else if (statStr == QLatin1String("CONFIRMED")) {
1593         status = Attendee::Accepted;
1594     } else if (statStr == QLatin1String("DECLINED")) {
1595         status = Attendee::Declined;
1596     } else if (statStr == QLatin1String("COMPLETED")) {
1597         status = Attendee::Completed;
1598     } else if (statStr == QLatin1String("DELEGATED")) {
1599         status = Attendee::Delegated;
1600     } else {
1601         qCDebug(KCALCORE_LOG) << "error setting attendee mStatus, unknown mStatus!";
1602         status = Attendee::NeedsAction;
1603     }
1604 
1605     return status;
1606 }
1607 
1608 QByteArray VCalFormat::writeStatus(Attendee::PartStat status) const
1609 {
1610     switch (status) {
1611     default:
1612     case Attendee::NeedsAction:
1613         return "NEEDS ACTION";
1614     case Attendee::Accepted:
1615         return "ACCEPTED";
1616     case Attendee::Declined:
1617         return "DECLINED";
1618     case Attendee::Tentative:
1619         return "TENTATIVE";
1620     case Attendee::Delegated:
1621         return "DELEGATED";
1622     case Attendee::Completed:
1623         return "COMPLETED";
1624     case Attendee::InProcess:
1625         return "NEEDS ACTION";
1626     }
1627 }
1628 
1629 void VCalFormat::readCustomProperties(VObject *o, const Incidence::Ptr &i)
1630 {
1631     VObjectIterator iter;
1632     char *s;
1633 
1634     initPropIterator(&iter, o);
1635     while (moreIteration(&iter)) {
1636         VObject *cur = nextVObject(&iter);
1637         const char *curname = vObjectName(cur);
1638         Q_ASSERT(curname);
1639         if ((curname[0] == 'X' && curname[1] == '-') && strcmp(curname, ICOrganizerProp) != 0) {
1640             // TODO - for the time being, we ignore the parameters part
1641             // and just do the value handling here
1642             i->setNonKDECustomProperty(curname, QString::fromUtf8(s = fakeCString(vObjectUStringZValue(cur))));
1643             deleteStr(s);
1644         }
1645     }
1646 }
1647 
1648 void VCalFormat::writeCustomProperties(VObject *o, const Incidence::Ptr &i)
1649 {
1650     Q_D(VCalFormat);
1651     const QMap<QByteArray, QString> custom = i->customProperties();
1652     for (auto cIt = custom.cbegin(); cIt != custom.cend(); ++cIt) {
1653         const QByteArray property = cIt.key();
1654         if (d->mManuallyWrittenExtensionFields.contains(property) || property.startsWith("X-KDE-VOLATILE")) { // krazy:exclude=strings
1655             continue;
1656         }
1657 
1658         addPropValue(o, property.constData(), cIt.value().toUtf8().constData());
1659     }
1660 }