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