File indexing completed on 2024-05-12 05:10:35

0001 /*
0002   SPDX-FileCopyrightText: 2013 Sérgio Martins <iamsergio@gmail.com>
0003 
0004   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0005 */
0006 
0007 #include "calendarjanitor.h"
0008 #include "collectionloader.h"
0009 
0010 #include <Akonadi/CalendarUtils>
0011 
0012 #include <KCalendarCore/Alarm>
0013 #include <KCalendarCore/Attachment>
0014 #include <KCalendarCore/Event>
0015 #include <KCalendarCore/Journal>
0016 #include <KCalendarCore/Todo>
0017 
0018 #include <KLocalizedString>
0019 
0020 #include <QCoreApplication>
0021 #include <QList>
0022 #include <QTextStream>
0023 
0024 #define TEXT_WIDTH 75
0025 
0026 static void print(const QString &message, bool newline = true)
0027 {
0028     QTextStream out(stdout);
0029     out << message;
0030     if (newline) {
0031         out << "\n";
0032     }
0033 }
0034 
0035 static void bailOut()
0036 {
0037     print(i18n("Bailing out. Fix your akonadi setup first. These kind of errors should not happen."));
0038     qApp->exit(-1);
0039 }
0040 
0041 static bool collectionIsReadOnly(const Akonadi::Collection &collection)
0042 {
0043     return !(collection.rights() & Akonadi::Collection::CanChangeItem) || !(collection.rights() & Akonadi::Collection::CanDeleteItem);
0044 }
0045 
0046 static bool incidenceIsOld(const KCalendarCore::Incidence::Ptr &incidence)
0047 {
0048     if (incidence->recurs() || incidence->type() == KCalendarCore::Incidence::TypeJournal) {
0049         return false;
0050     }
0051 
0052     QDateTime datetime = incidence->dtStart();
0053     if (!datetime.isValid() && incidence->type() == KCalendarCore::Incidence::TypeTodo) {
0054         datetime = incidence->dateTime(KCalendarCore::Incidence::RoleEnd);
0055     }
0056 
0057     return datetime.isValid() && datetime.daysTo(QDateTime::currentDateTime()) > 365;
0058 }
0059 
0060 CalendarJanitor::CalendarJanitor(const Options &options, QObject *parent)
0061     : QObject(parent)
0062     , m_collectionLoader(new CollectionLoader(this))
0063     , m_options(options)
0064     , m_currentSanityCheck(Options::CheckNone)
0065     , m_pendingModifications(0)
0066     , m_pendingDeletions(0)
0067     , m_strippingOldAlarms(false)
0068     , m_returnCode(0)
0069 {
0070     m_changer = new Akonadi::IncidenceChanger(this);
0071     m_changer->setShowDialogsOnError(false);
0072     connect(m_changer, &Akonadi::IncidenceChanger::modifyFinished, this, &CalendarJanitor::onModifyFinished);
0073     connect(m_changer, &Akonadi::IncidenceChanger::deleteFinished, this, &CalendarJanitor::onDeleteFinished);
0074     connect(m_collectionLoader, &CollectionLoader::loaded, this, &CalendarJanitor::onCollectionsFetched);
0075 }
0076 
0077 void CalendarJanitor::start()
0078 {
0079     m_collectionLoader->load();
0080 }
0081 
0082 void CalendarJanitor::onCollectionsFetched(bool success)
0083 {
0084     if (!success) {
0085         print(i18n("Error while fetching collections"));
0086         Q_EMIT finished(false);
0087         qApp->exit(-1);
0088         return;
0089     }
0090 
0091     const auto collections = m_collectionLoader->collections();
0092     for (const Akonadi::Collection &collection : collections) {
0093         if (m_options.testCollection(collection.id())) {
0094             m_collectionsToProcess << collection;
0095         }
0096     }
0097 
0098     if (m_collectionsToProcess.isEmpty()) {
0099         print(i18n("There are no collections to process."));
0100         qApp->exit((-1));
0101         return;
0102     }
0103 
0104     // Load all items:
0105     m_calendar = Akonadi::FetchJobCalendar::Ptr(new Akonadi::FetchJobCalendar());
0106     connect(m_calendar.data(), &Akonadi::FetchJobCalendar::loadFinished, this, &CalendarJanitor::onItemsFetched);
0107 }
0108 
0109 void CalendarJanitor::onItemsFetched(bool success, const QString &errorMessage)
0110 {
0111     if (!success) {
0112         print(errorMessage);
0113         Q_EMIT finished(false);
0114         qApp->exit(-1);
0115         return;
0116     }
0117 
0118     // Start processing collections
0119     processNextCollection();
0120 }
0121 
0122 void CalendarJanitor::onModifyFinished(int changeId, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorMessage)
0123 {
0124     Q_UNUSED(changeId)
0125     if (resultCode != Akonadi::IncidenceChanger::ResultCodeSuccess) {
0126         print(i18n("Error while modifying incidence: %1", errorMessage));
0127         bailOut();
0128         return;
0129     }
0130     if (!m_options.stripOldAlarms()) {
0131         print(i18n("Fixed item %1", item.id()));
0132     }
0133 
0134     m_pendingModifications--;
0135     if (m_pendingModifications == 0) {
0136         runNextTest();
0137     }
0138 }
0139 
0140 void CalendarJanitor::onDeleteFinished(int changeId,
0141                                        const QList<Akonadi::Item::Id> &items,
0142                                        Akonadi::IncidenceChanger::ResultCode resultCode,
0143                                        const QString &errorMessage)
0144 {
0145     Q_UNUSED(changeId)
0146     if (resultCode != Akonadi::IncidenceChanger::ResultCodeSuccess) {
0147         print(i18n("Error while deleting incidence: %1", errorMessage));
0148         bailOut();
0149         return;
0150     }
0151     print(i18n("Deleted item %1", items.first()));
0152     m_pendingDeletions--;
0153     if (m_pendingDeletions == 0) {
0154         runNextTest();
0155     }
0156 }
0157 
0158 void CalendarJanitor::processNextCollection()
0159 {
0160     m_itemsToProcess.clear();
0161     m_currentSanityCheck = Options::CheckNone;
0162     m_strippingOldAlarms = false;
0163 
0164     if (m_collectionsToProcess.isEmpty()) {
0165         print(QLatin1Char('\n') + QString().leftJustified(TEXT_WIDTH, QLatin1Char('*')));
0166         Q_EMIT finished(true);
0167         qApp->exit(m_returnCode);
0168         return;
0169     }
0170 
0171     m_currentCollection = m_collectionsToProcess.takeFirst();
0172     print(QLatin1Char('\n') + QString().leftJustified(TEXT_WIDTH, QLatin1Char('*')));
0173     print(i18n("Processing collection %1 (id=%2)...", m_currentCollection.displayName(), m_currentCollection.id()));
0174 
0175     if (collectionIsReadOnly(m_currentCollection)) {
0176         if (m_options.action() == Options::ActionScanAndFix) {
0177             print(i18n("Collection is read only, disabling fix mode."));
0178         } else if (m_options.stripOldAlarms()) {
0179             print(i18n("Collection is read only, skipping it."));
0180             processNextCollection();
0181             return;
0182         }
0183     }
0184 
0185     m_itemsToProcess = m_calendar->items(m_currentCollection.id());
0186     if (m_itemsToProcess.isEmpty()) {
0187         print(i18n("Collection is empty, ignoring it."));
0188         processNextCollection();
0189     } else {
0190         m_incidenceMap.clear();
0191         for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0192             KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0193             Q_ASSERT(incidence);
0194             m_incidenceMap.insert(incidence->instanceIdentifier(), incidence);
0195             m_incidenceToItem.insert(incidence, item);
0196         }
0197         runNextTest();
0198     }
0199 }
0200 
0201 void CalendarJanitor::runNextTest()
0202 {
0203     if (m_options.stripOldAlarms()) {
0204         if (!m_strippingOldAlarms) {
0205             m_strippingOldAlarms = true;
0206             stripOldAlarms();
0207         } else {
0208             processNextCollection();
0209         }
0210 
0211         return;
0212     }
0213 
0214     int currentType = static_cast<int>(m_currentSanityCheck);
0215     m_currentSanityCheck = static_cast<Options::SanityCheck>(currentType + 1);
0216 
0217     switch (m_currentSanityCheck) {
0218     case Options::CheckEmptySummary:
0219         sanityCheck1();
0220         break;
0221     case Options::CheckEmptyUid:
0222         sanityCheck2();
0223         break;
0224     case Options::CheckEventDates:
0225         sanityCheck3();
0226         break;
0227     case Options::CheckTodoDates:
0228         sanityCheck4();
0229         break;
0230     case Options::CheckJournalDates:
0231         sanityCheck5();
0232         break;
0233     case Options::CheckOrphans:
0234         sanityCheck6();
0235         break;
0236     case Options::CheckDuplicateUIDs:
0237         sanityCheck7();
0238         break;
0239     case Options::CheckStats:
0240         sanityCheck8();
0241         break;
0242     case Options::CheckOrphanRecurId:
0243         sanityCheck9();
0244         break;
0245     case Options::CheckCount:
0246         processNextCollection();
0247         break;
0248     default:
0249         Q_ASSERT(false);
0250     }
0251 }
0252 
0253 void CalendarJanitor::sanityCheck1()
0254 {
0255     beginTest(i18n("Checking for incidences with empty summary and description..."));
0256 
0257     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0258         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0259         if (incidence->summary().isEmpty() && incidence->description().isEmpty() && incidence->attachments().isEmpty()) {
0260             printFound(item);
0261             deleteIncidence(item);
0262         }
0263     }
0264 
0265     endTest();
0266 }
0267 
0268 void CalendarJanitor::sanityCheck2()
0269 {
0270     beginTest(i18n("Checking for incidences with empty UID..."));
0271 
0272     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0273         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0274         if (incidence->uid().isEmpty()) {
0275             printFound(item);
0276             if (m_fixingEnabled) {
0277                 incidence->recreate();
0278                 m_pendingModifications++;
0279                 m_changer->modifyIncidence(item);
0280             }
0281         }
0282     }
0283 
0284     endTest();
0285 }
0286 
0287 void CalendarJanitor::sanityCheck3()
0288 {
0289     beginTest(i18n("Checking for events with invalid DTSTART..."));
0290     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0291         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0292         KCalendarCore::Event::Ptr event = incidence.dynamicCast<KCalendarCore::Event>();
0293         if (!event) {
0294             continue;
0295         }
0296 
0297         QDateTime start = event->dtStart();
0298         QDateTime end = event->dtEnd();
0299 
0300         bool modify = false;
0301         QString message;
0302         if (!start.isValid() && end.isValid()) {
0303             modify = true;
0304             printFound(item);
0305             event->setDtStart(end);
0306         } else if (!start.isValid() && !end.isValid()) {
0307             modify = true;
0308             printFound(item);
0309             event->setDtStart(QDateTime::currentDateTime());
0310             event->setDtEnd(event->dtStart().addSecs(3600));
0311         }
0312 
0313         if (modify) {
0314             if (m_fixingEnabled) {
0315                 m_changer->modifyIncidence(item);
0316                 m_pendingModifications++;
0317             }
0318         }
0319     }
0320 
0321     endTest();
0322 }
0323 
0324 void CalendarJanitor::sanityCheck4()
0325 {
0326     beginTest(i18n("Checking for recurring to-dos with invalid DTSTART..."));
0327     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0328         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0329         KCalendarCore::Todo::Ptr todo = incidence.dynamicCast<KCalendarCore::Todo>();
0330         if (!todo) {
0331             continue;
0332         }
0333 
0334         QDateTime start = todo->dtStart();
0335         QDateTime due = todo->dtDue();
0336         bool modify = false;
0337         if (todo->recurs() && !start.isValid() && due.isValid()) {
0338             modify = true;
0339             printFound(item);
0340             todo->setDtStart(due);
0341         }
0342 
0343         if (todo->recurs() && !start.isValid() && !due.isValid()) {
0344             modify = true;
0345             printFound(item);
0346             todo->setDtStart(QDateTime::currentDateTime());
0347         }
0348 
0349         if (modify) {
0350             if (m_fixingEnabled) {
0351                 m_changer->modifyIncidence(item);
0352                 m_pendingModifications++;
0353             }
0354         }
0355     }
0356 
0357     endTest();
0358 }
0359 
0360 void CalendarJanitor::sanityCheck5()
0361 {
0362     beginTest(i18n("Checking for journals with invalid DTSTART..."));
0363     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0364         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0365         if (incidence->type() != KCalendarCore::Incidence::TypeJournal) {
0366             continue;
0367         }
0368 
0369         if (!incidence->dtStart().isValid()) {
0370             printFound(item);
0371             incidence->setDtStart(QDateTime::currentDateTime());
0372             if (m_fixingEnabled) {
0373                 m_changer->modifyIncidence(item);
0374                 m_pendingModifications++;
0375             }
0376         }
0377     }
0378     endTest();
0379 }
0380 
0381 void CalendarJanitor::sanityCheck6()
0382 {
0383     beginTest(i18n("Checking for orphans...")); // Incidences without a parent
0384 
0385     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0386         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0387         const QString parentUid = incidence->relatedTo();
0388         if (!parentUid.isEmpty() && !m_calendar->incidence(parentUid)) {
0389             printFound(item, i18n("The following incidences are children of nonexistent parents"));
0390             if (m_fixingEnabled) {
0391                 incidence->setRelatedTo(QString());
0392                 m_changer->modifyIncidence(item);
0393                 m_pendingModifications++;
0394             }
0395         }
0396     }
0397 
0398     endTest(true, i18n("In fix mode these children will be unparented."), i18n("Children were successfully unparented."));
0399 }
0400 
0401 void CalendarJanitor::sanityCheck7()
0402 {
0403     beginTest(i18n("Checking for duplicate UIDs..."));
0404 
0405     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0406         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0407         const QList<KCalendarCore::Incidence::Ptr> existingIncidences = m_incidenceMap.values(incidence->instanceIdentifier());
0408 
0409         if (existingIncidences.count() == 1) {
0410             continue;
0411         }
0412 
0413         for (const KCalendarCore::Incidence::Ptr &existingIncidence : existingIncidences) {
0414             if (existingIncidence != incidence && *incidence == *existingIncidence) {
0415                 printFound(item);
0416                 deleteIncidence(item);
0417                 break;
0418             }
0419         }
0420     }
0421 
0422     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0423         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0424         const QList<KCalendarCore::Incidence::Ptr> existingIncidences = m_incidenceMap.values(incidence->instanceIdentifier());
0425 
0426         if (existingIncidences.count() == 1) {
0427             continue;
0428         }
0429 
0430         for (int i = 1; i < existingIncidences.count(); ++i) {
0431             printFound(item);
0432             if (m_fixingEnabled) {
0433                 KCalendarCore::Incidence::Ptr existingIncidence = existingIncidences.at(i);
0434                 Akonadi::Item item = m_incidenceToItem.value(existingIncidence);
0435                 Q_ASSERT(item.isValid());
0436                 if (item.isValid()) {
0437                     existingIncidence->recreate();
0438                     m_changer->modifyIncidence(item);
0439                     m_pendingModifications++;
0440                     m_incidenceMap.remove(incidence->instanceIdentifier(), existingIncidence);
0441                 }
0442             }
0443         }
0444     }
0445 
0446     endTest();
0447 }
0448 
0449 static void printStat(const QString &message, int arg)
0450 {
0451     if (arg > 0) {
0452         print(message.leftJustified(50), false);
0453         const QString s = QStringLiteral(": %1");
0454         print(s.arg(arg));
0455     }
0456 }
0457 
0458 void CalendarJanitor::sanityCheck8()
0459 {
0460     beginTest(i18n("Gathering statistics..."));
0461     print(QStringLiteral("\n"));
0462 
0463     int numOldAlarms = 0;
0464     int numAttachments = 0;
0465     int totalAttachmentSize = 0;
0466     int numOldIncidences = 0;
0467     int numEmptyRID = 0;
0468     QHash<KCalendarCore::Incidence::IncidenceType, int> m_counts;
0469 
0470     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0471         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0472         if (!incidence->attachments().isEmpty()) {
0473             const auto attachments = incidence->attachments();
0474             for (const KCalendarCore::Attachment &attachment : attachments) {
0475                 if (!attachment.isUri()) {
0476                     numAttachments++;
0477                     totalAttachmentSize += attachment.size();
0478                 }
0479             }
0480         }
0481 
0482         m_counts[incidence->type()]++;
0483 
0484         if (incidenceIsOld(incidence)) {
0485             if (!incidence->alarms().isEmpty()) {
0486                 numOldAlarms++;
0487             }
0488             numOldIncidences++;
0489         }
0490 
0491         numAttachments += incidence->attachments().count();
0492 
0493         if (item.remoteId().isEmpty()) {
0494             numEmptyRID++;
0495         }
0496     }
0497 
0498     printStat(i18n("Events"), m_counts[KCalendarCore::Incidence::TypeEvent]);
0499     printStat(i18n("Todos"), m_counts[KCalendarCore::Incidence::TypeTodo]);
0500     printStat(i18n("Journals"), m_counts[KCalendarCore::Incidence::TypeJournal]);
0501     printStat(i18n("Passed events and to-dos (>365 days)"), numOldIncidences);
0502     printStat(i18n("Old incidences with alarms"), numOldAlarms);
0503     printStat(i18n("Inline attachments"), numAttachments);
0504     printStat(i18n("Items with empty remote id [!!]"), numEmptyRID);
0505 
0506     if (totalAttachmentSize < 1024) {
0507         printStat(i18n("Total size of inline attachments (bytes)"), totalAttachmentSize);
0508     } else {
0509         printStat(i18n("Total size of inline attachments (KB)"), totalAttachmentSize / 1024);
0510     }
0511 
0512     if (numEmptyRID > 0) {
0513         m_returnCode = -2;
0514     }
0515 
0516     endTest(/**print=*/false);
0517 }
0518 
0519 void CalendarJanitor::sanityCheck9()
0520 {
0521     beginTest(i18n("Checking for RECURRING-ID incidences with nonexistent master incidence..."));
0522     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0523         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0524         if (incidence->recurs() && incidence->hasRecurrenceId() && !m_calendar->incidence(incidence->uid())) {
0525             printFound(item);
0526             if (m_fixingEnabled) {
0527                 bool modified = false;
0528 
0529                 QDateTime recId = incidence->recurrenceId();
0530                 QDateTime start = incidence->dtStart();
0531                 QDateTime end = incidence->dateTime(KCalendarCore::Incidence::RoleEnd);
0532 
0533                 KCalendarCore::Event::Ptr event = incidence.dynamicCast<KCalendarCore::Event>();
0534                 KCalendarCore::Todo::Ptr todo = incidence.dynamicCast<KCalendarCore::Todo>();
0535 
0536                 if (event && start.isValid() && end.isValid()) {
0537                     modified = true;
0538                     const int duration = start.daysTo(end.toTimeSpec(start.timeSpec()));
0539                     incidence->setDtStart(recId);
0540                     event->setDtEnd(recId.addDays(duration));
0541                 } else if (todo && start.isValid()) {
0542                     modified = true;
0543                     incidence->setDtStart(recId);
0544 
0545                     if (end.isValid()) {
0546                         const int duration = start.daysTo(end.toTimeSpec(start.timeSpec()));
0547                         todo->setDtDue(recId.addDays(duration));
0548                     }
0549                 }
0550 
0551                 if (modified) {
0552                     m_pendingModifications++;
0553                     incidence->recreate(); // change uid
0554                     incidence->clearRecurrence(); // make it non-recurring
0555                     incidence->setRecurrenceId(QDateTime());
0556                     m_changer->modifyIncidence(item);
0557                 }
0558             }
0559         }
0560     }
0561 
0562     endTest(true, i18n("In fix mode the RECURRING-ID property will be unset and UID changed."), i18n("Recurrence cleared."));
0563 }
0564 
0565 void CalendarJanitor::stripOldAlarms()
0566 {
0567     beginTest(i18n("Deleting alarms older than 365 days..."));
0568 
0569     for (const Akonadi::Item &item : std::as_const(m_itemsToProcess)) {
0570         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0571         if (!incidence->alarms().isEmpty() && incidenceIsOld(incidence)) {
0572             incidence->clearAlarms();
0573             m_pendingModifications++;
0574             m_changer->modifyIncidence(item);
0575         }
0576     }
0577 
0578     endTest();
0579 }
0580 
0581 static QString dateString(const KCalendarCore::Incidence::Ptr &incidence)
0582 {
0583     QDateTime start = incidence->dtStart();
0584     QDateTime end = incidence->dateTime(KCalendarCore::Incidence::RoleEnd);
0585     QString str = QLatin1StringView("DTSTART=") + (start.isValid() ? start.toString() : i18n("invalid")) + QLatin1StringView("; ");
0586 
0587     if (incidence->type() == KCalendarCore::Incidence::TypeJournal) {
0588         return str;
0589     }
0590 
0591     str += QLatin1StringView("\n        ");
0592 
0593     if (incidence->type() == KCalendarCore::Incidence::TypeTodo) {
0594         str += QLatin1StringView("DTDUE=");
0595     } else if (incidence->type() == KCalendarCore::Incidence::TypeEvent) {
0596         str += QLatin1StringView("DTEND=");
0597     }
0598 
0599     str += (start.isValid() ? end.toString() : i18n("invalid")) + QLatin1StringView("; ");
0600 
0601     if (incidence->recurs()) {
0602         str += i18n("recurrent");
0603     }
0604 
0605     return str;
0606 }
0607 
0608 void CalendarJanitor::printFound(const Akonadi::Item &item, const QString &explanation)
0609 {
0610     KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0611     m_numDamaged++;
0612     if (m_numDamaged == 1) {
0613         print(QStringLiteral(" [!!]"));
0614         if (!explanation.isEmpty()) {
0615             print(QStringLiteral("    "), false);
0616             print(explanation, false);
0617             print(QStringLiteral(":\n"));
0618         }
0619     }
0620     print(QLatin1StringView("    * ") + i18n("Found buggy incidence:"));
0621     print(QLatin1StringView("        ") + i18n("id=%1; summary=\"%2\"", item.id(), incidence->summary()));
0622     print(QLatin1StringView("        ") + dateString(incidence));
0623 }
0624 
0625 void CalendarJanitor::beginTest(const QString &message)
0626 {
0627     m_numDamaged = 0;
0628     m_fixingEnabled = m_options.action() == Options::ActionScanAndFix && !collectionIsReadOnly(m_currentCollection);
0629     print(message.leftJustified(TEXT_WIDTH), false);
0630 }
0631 
0632 void CalendarJanitor::endTest(bool printEnabled, const QString &fixExplanation, const QString &fixExplanation2)
0633 {
0634     if (m_numDamaged == 0 && printEnabled) {
0635         print(QStringLiteral(" [OK]"));
0636     } else if (m_numDamaged > 0) {
0637         print(QStringLiteral("\n    "), false);
0638         if (m_options.action() == Options::ActionScanAndFix) {
0639             print(fixExplanation2);
0640         } else {
0641             print(fixExplanation);
0642         }
0643 
0644         print(QString());
0645     }
0646 
0647     if (m_pendingDeletions == 0 && m_pendingModifications == 0) {
0648         runNextTest();
0649     }
0650 }
0651 
0652 void CalendarJanitor::deleteIncidence(const Akonadi::Item &item)
0653 {
0654     if (m_fixingEnabled && !collectionIsReadOnly(m_currentCollection)) {
0655         m_pendingDeletions++;
0656         m_changer->deleteIncidence(item);
0657         KCalendarCore::Incidence::Ptr incidence = Akonadi::CalendarUtils::incidence(item);
0658         m_incidenceMap.remove(incidence->instanceIdentifier(), incidence);
0659         m_incidenceToItem.remove(incidence);
0660     }
0661 }
0662 
0663 #include "moc_calendarjanitor.cpp"