File indexing completed on 2024-05-12 05:13:14

0001 /*
0002   SPDX-FileCopyrightText: 2000, 2001 Cornelius Schumacher <schumacher@kde.org>
0003   SPDX-FileCopyrightText: 2004 David Faure <faure@kde.org>
0004   SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <reinhold@kainhofer.com>
0005 
0006   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0007 */
0008 
0009 #include "eventarchiver.h"
0010 
0011 #include "kcalprefs.h"
0012 
0013 #include <Akonadi/CalendarUtils>
0014 #include <Akonadi/IncidenceChanger>
0015 
0016 #include <KCalendarCore/FileStorage>
0017 #include <KCalendarCore/ICalFormat>
0018 #include <KCalendarCore/MemoryCalendar>
0019 
0020 #include <KCalUtils/Stringify>
0021 
0022 #include "calendarsupport_debug.h"
0023 #include <KIO/FileCopyJob>
0024 #include <KIO/StatJob>
0025 #include <KJobWidgets>
0026 #include <KLocalizedString>
0027 #include <KMessageBox>
0028 
0029 #include <QLocale>
0030 #include <QTemporaryFile>
0031 #include <QTimeZone>
0032 
0033 using namespace KCalendarCore;
0034 using namespace KCalUtils;
0035 using namespace CalendarSupport;
0036 
0037 class GroupwareScoppedDisabler
0038 {
0039 public:
0040     GroupwareScoppedDisabler(Akonadi::IncidenceChanger *changer)
0041         : m_changer(changer)
0042     {
0043         m_wasEnabled = m_changer->groupwareCommunication();
0044         m_changer->setGroupwareCommunication(false);
0045     }
0046 
0047     ~GroupwareScoppedDisabler()
0048     {
0049         m_changer->setGroupwareCommunication(m_wasEnabled);
0050     }
0051 
0052     bool m_wasEnabled = false;
0053     Akonadi::IncidenceChanger *const m_changer;
0054 };
0055 
0056 EventArchiver::EventArchiver(QObject *parent)
0057     : QObject(parent)
0058 {
0059 }
0060 
0061 EventArchiver::~EventArchiver() = default;
0062 
0063 void EventArchiver::runOnce(const Akonadi::ETMCalendar::Ptr &calendar, Akonadi::IncidenceChanger *changer, QDate limitDate, QWidget *widget)
0064 {
0065     run(calendar, changer, limitDate, widget, true, true);
0066 }
0067 
0068 void EventArchiver::runAuto(const Akonadi::ETMCalendar::Ptr &calendar, Akonadi::IncidenceChanger *changer, QWidget *widget, bool withGUI)
0069 {
0070     QDate limitDate(QDate::currentDate());
0071     const int expiryTime = KCalPrefs::instance()->mExpiryTime;
0072     switch (KCalPrefs::instance()->mExpiryUnit) {
0073     case KCalPrefs::UnitDays: // Days
0074         limitDate = limitDate.addDays(-expiryTime);
0075         break;
0076     case KCalPrefs::UnitWeeks: // Weeks
0077         limitDate = limitDate.addDays(-expiryTime * 7);
0078         break;
0079     case KCalPrefs::UnitMonths: // Months
0080         limitDate = limitDate.addMonths(-expiryTime);
0081         break;
0082     default:
0083         return;
0084     }
0085     run(calendar, changer, limitDate, widget, withGUI, false);
0086 }
0087 
0088 void EventArchiver::run(const Akonadi::ETMCalendar::Ptr &calendar,
0089                         Akonadi::IncidenceChanger *changer,
0090                         QDate limitDate,
0091                         QWidget *widget,
0092                         bool withGUI,
0093                         bool errorIfNone)
0094 {
0095     GroupwareScoppedDisabler disabler(changer); // Disables groupware communication temporarily
0096 
0097     // We need to use rawEvents, otherwise events hidden by filters will not be archived.
0098     KCalendarCore::Event::List events;
0099     KCalendarCore::Todo::List todos;
0100     KCalendarCore::Journal::List journals;
0101 
0102     if (KCalPrefs::instance()->mArchiveEvents) {
0103         events = calendar->rawEvents(QDate(1769, 12, 1),
0104                                      // #29555, also advertised by the "limitDate not included" in the class docu
0105                                      limitDate.addDays(-1),
0106                                      QTimeZone::systemTimeZone(),
0107                                      true);
0108     }
0109     if (KCalPrefs::instance()->mArchiveTodos) {
0110         const KCalendarCore::Todo::List rawTodos = calendar->rawTodos();
0111 
0112         for (const KCalendarCore::Todo::Ptr &todo : rawTodos) {
0113             Q_ASSERT(todo);
0114             if (isSubTreeComplete(calendar, todo, limitDate)) {
0115                 todos.append(todo);
0116             }
0117         }
0118     }
0119 
0120     const KCalendarCore::Incidence::List incidences = calendar->mergeIncidenceList(events, todos, journals);
0121 
0122     qCDebug(CALENDARSUPPORT_LOG) << "archiving incidences before" << limitDate << " ->" << incidences.count() << " incidences found.";
0123     if (incidences.isEmpty()) {
0124         if (withGUI && errorIfNone) {
0125             KMessageBox::information(widget,
0126                                      i18n("There are no items before %1", QLocale::system().toString(limitDate, QLocale::ShortFormat)),
0127                                      i18nc("@title:window", "Archive"),
0128                                      QStringLiteral("ArchiverNoIncidences"));
0129         }
0130         return;
0131     }
0132 
0133     switch (KCalPrefs::instance()->mArchiveAction) {
0134     case KCalPrefs::actionDelete:
0135         deleteIncidences(changer, limitDate, widget, calendar->itemList(incidences), withGUI);
0136         break;
0137     case KCalPrefs::actionArchive:
0138         archiveIncidences(calendar, changer, limitDate, widget, incidences, withGUI);
0139         break;
0140     }
0141 }
0142 
0143 void EventArchiver::deleteIncidences(Akonadi::IncidenceChanger *changer, QDate limitDate, QWidget *widget, const Akonadi::Item::List &items, bool withGUI)
0144 {
0145     QStringList incidenceStrs;
0146     Akonadi::Item::List::ConstIterator it;
0147     Akonadi::Item::List::ConstIterator end(items.constEnd());
0148     incidenceStrs.reserve(items.count());
0149     for (it = items.constBegin(); it != end; ++it) {
0150         incidenceStrs.append(Akonadi::CalendarUtils::incidence(*it)->summary());
0151     }
0152 
0153     if (withGUI) {
0154         const int result = KMessageBox::warningContinueCancelList(widget,
0155                                                                   i18n("Delete all items before %1 without saving?\n"
0156                                                                        "The following items will be deleted:",
0157                                                                        QLocale::system().toString(limitDate, QLocale::ShortFormat)),
0158                                                                   incidenceStrs,
0159                                                                   i18nc("@title:window", "Delete Old Items"),
0160                                                                   KStandardGuiItem::del());
0161         if (result != KMessageBox::Continue) {
0162             return;
0163         }
0164     }
0165 
0166     changer->deleteIncidences(items, /**parent=*/widget);
0167 
0168     // TODO: Q_EMIT only after hearing back from incidence changer
0169     Q_EMIT eventsDeleted();
0170 }
0171 
0172 void EventArchiver::archiveIncidences(const Akonadi::ETMCalendar::Ptr &calendar,
0173                                       Akonadi::IncidenceChanger *changer,
0174                                       QDate limitDate,
0175                                       QWidget *widget,
0176                                       const KCalendarCore::Incidence::List &incidences,
0177                                       bool withGUI)
0178 {
0179     Q_UNUSED(limitDate)
0180     Q_UNUSED(withGUI)
0181 
0182     FileStorage storage(calendar);
0183 
0184     QString tmpFileName;
0185     // KSaveFile cannot be called with an open File Handle on Windows.
0186     // So we use QTemporaryFile only to generate a unique filename
0187     // and then close/delete the file again. This file must be deleted
0188     // here.
0189     {
0190         QTemporaryFile tmpFile;
0191         tmpFile.open();
0192         tmpFileName = tmpFile.fileName();
0193     }
0194     // Save current calendar to disk
0195     storage.setFileName(tmpFileName);
0196     if (!storage.save()) {
0197         qCDebug(CALENDARSUPPORT_LOG) << "Can't save calendar to temp file";
0198         return;
0199     }
0200 
0201     // Duplicate current calendar by loading in new calendar object
0202     MemoryCalendar::Ptr archiveCalendar(new MemoryCalendar(QTimeZone::systemTimeZone()));
0203 
0204     FileStorage archiveStore(archiveCalendar);
0205     archiveStore.setFileName(tmpFileName);
0206     auto format = new ICalFormat();
0207     archiveStore.setSaveFormat(format);
0208     if (!archiveStore.load()) {
0209         qCDebug(CALENDARSUPPORT_LOG) << "Can't load calendar from temp file";
0210         QFile::remove(tmpFileName);
0211         return;
0212     }
0213 
0214     // Strip active events from calendar so that only events to be archived
0215     // remain. This is not really efficient, but there is no other easy way.
0216     QStringList uids;
0217     Incidence::List allIncidences = archiveCalendar->rawIncidences();
0218     uids.reserve(incidences.count());
0219     for (const KCalendarCore::Incidence::Ptr &incidence : std::as_const(incidences)) {
0220         uids.append(incidence->uid());
0221     }
0222     for (const KCalendarCore::Incidence::Ptr &incidence : std::as_const(allIncidences)) {
0223         if (!uids.contains(incidence->uid())) {
0224             archiveCalendar->deleteIncidence(incidence);
0225         }
0226     }
0227 
0228     // Get or create the archive file
0229     QUrl archiveURL(KCalPrefs::instance()->mArchiveFile);
0230     QString archiveFile;
0231     QTemporaryFile downloadTempFile;
0232 
0233     bool fileExists = false;
0234     if (archiveURL.isLocalFile()) {
0235         fileExists = QFile::exists(archiveURL.toLocalFile());
0236     } else {
0237         auto job = KIO::stat(archiveURL, KIO::StatJob::SourceSide, KIO::StatBasic);
0238 
0239         KJobWidgets::setWindow(job, widget);
0240         fileExists = job->exec();
0241     }
0242 
0243     if (fileExists) {
0244         archiveFile = downloadTempFile.fileName();
0245         auto job = KIO::file_copy(archiveURL, QUrl::fromLocalFile(archiveFile));
0246         KJobWidgets::setWindow(job, widget);
0247         if (!job->exec()) {
0248             qCDebug(CALENDARSUPPORT_LOG) << "Can't download archive file";
0249             QFile::remove(tmpFileName);
0250             return;
0251         }
0252         // Merge with events to be archived.
0253         archiveStore.setFileName(archiveFile);
0254         if (!archiveStore.load()) {
0255             qCDebug(CALENDARSUPPORT_LOG) << "Can't merge with archive file";
0256             QFile::remove(tmpFileName);
0257             return;
0258         }
0259     } else {
0260         archiveFile = tmpFileName;
0261     }
0262 
0263     // Save archive calendar
0264     if (!archiveStore.save()) {
0265         QString errmess;
0266         if (format->exception()) {
0267             errmess = Stringify::errorMessage(*format->exception());
0268         } else {
0269             errmess = i18nc("save failure cause unknown", "Reason unknown");
0270         }
0271         KMessageBox::error(widget, i18n("Cannot write archive file %1. %2", archiveStore.fileName(), errmess));
0272         QFile::remove(tmpFileName);
0273         return;
0274     }
0275 
0276     // Upload if necessary
0277     QUrl srcUrl = QUrl::fromLocalFile(archiveFile);
0278     if (srcUrl != archiveURL) {
0279         auto job = KIO::file_copy(QUrl::fromLocalFile(archiveFile), archiveURL);
0280         KJobWidgets::setWindow(job, widget);
0281         if (!job->exec()) {
0282             KMessageBox::error(widget, i18n("Cannot write archive. %1", job->errorString()));
0283             QFile::remove(tmpFileName);
0284             return;
0285         }
0286     }
0287 
0288     QFile::remove(tmpFileName);
0289 
0290     // We don't want it to ask to send invitations for each incidence.
0291     changer->startAtomicOperation(i18n("Archiving events"));
0292 
0293     // Delete archived events from calendar
0294     const Akonadi::Item::List items = calendar->itemList(incidences);
0295     for (const Akonadi::Item &item : items) {
0296         changer->deleteIncidence(item, widget);
0297     } // TODO: Q_EMIT only after hearing back from incidence changer
0298     changer->endAtomicOperation();
0299 
0300     Q_EMIT eventsDeleted();
0301 }
0302 
0303 bool EventArchiver::isSubTreeComplete(const Akonadi::ETMCalendar::Ptr &calendar, const Todo::Ptr &todo, QDate limitDate, QStringList checkedUids) const
0304 {
0305     if (!todo->isCompleted() || todo->completed().date() >= limitDate) {
0306         return false;
0307     }
0308 
0309     // This QList is only to prevent infinite recursion
0310     if (checkedUids.contains(todo->uid())) {
0311         // Probably will never happen, calendar.cpp checks for this
0312         qCWarning(CALENDARSUPPORT_LOG) << "To-do hierarchy loop detected!";
0313         return false;
0314     }
0315 
0316     checkedUids.append(todo->uid());
0317     const KCalendarCore::Incidence::List children = calendar->childIncidences(todo->uid());
0318     for (const KCalendarCore::Incidence::Ptr &incidence : children) {
0319         const Todo::Ptr t = incidence.dynamicCast<KCalendarCore::Todo>();
0320         if (t && !isSubTreeComplete(calendar, t, limitDate, checkedUids)) {
0321             return false;
0322         }
0323     }
0324 
0325     return true;
0326 }
0327 
0328 #include "moc_eventarchiver.cpp"