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 }