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

0001 /*
0002  *  functions.cpp  -  miscellaneous functions
0003  *  Program:  kalarm
0004  *  SPDX-FileCopyrightText: 2001-2023 David Jarvie <djarvie@kde.org>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "functions.h"
0010 #include "functions_p.h"
0011 
0012 #include "displaycalendar.h"
0013 #include "kalarmapp.h"
0014 #include "kamail.h"
0015 #include "mainwindow.h"
0016 #include "preferences.h"
0017 #include "resourcescalendar.h"
0018 #include "resources/calendarfunctions.h"
0019 #include "resources/datamodel.h"
0020 #include "resources/eventmodel.h"
0021 #include "resources/resources.h"
0022 #include "lib/autoqpointer.h"
0023 #include "lib/dragdrop.h"
0024 #include "lib/filedialog.h"
0025 #include "lib/messagebox.h"
0026 #include "lib/shellprocess.h"
0027 #include "kalarmcalendar/identities.h"
0028 #include "akonadiplugin/akonadiplugin.h"
0029 #include "kalarm_debug.h"
0030 
0031 #include <KCalendarCore/Event>
0032 #include <KCalendarCore/ICalFormat>
0033 #include <KCalendarCore/Person>
0034 #include <KCalendarCore/Duration>
0035 #include <KCalendarCore/MemoryCalendar>
0036 using namespace KCalendarCore;
0037 #include <KIdentityManagementCore/IdentityManager>
0038 #include <KIdentityManagementCore/Identity>
0039 
0040 #include <KConfigGroup>
0041 #include <KSharedConfig>
0042 #include <KToggleAction>
0043 #include <KLocalizedString>
0044 
0045 #if ENABLE_RTC_WAKE_FROM_SUSPEND
0046 #include <KAuth/ActionReply>
0047 #include <KAuth/ExecuteJob>
0048 #endif
0049 #include <KStandardShortcut>
0050 #include <KFileItem>
0051 #include <KJobWidgets>
0052 #include <KIO/StatJob>
0053 #include <KIO/StoredTransferJob>
0054 #include <KFileCustomDialog>
0055 #include <KWindowSystem>
0056 #if ENABLE_X11
0057 #include <KWindowInfo>
0058 #include <KX11Extras>
0059 #endif
0060 
0061 #include <QAction>
0062 #include <QDBusConnectionInterface>
0063 #include <QDBusInterface>
0064 #include <QTimer>
0065 #include <QMimeData>
0066 #include <QStandardPaths>
0067 #include <QPushButton>
0068 #include <QTemporaryFile>
0069 
0070 //clazy:excludeall=non-pod-global-static
0071 
0072 namespace
0073 {
0074 bool refreshAlarmsQueued = false;
0075 QUrl lastImportUrl;     // last URL for Import Alarms file dialogue
0076 QUrl lastExportUrl;     // last URL for Export Alarms file dialogue
0077 
0078 struct UpdateStatusData
0079 {
0080     KAlarm::UpdateResult status;   // status code and error message if any
0081     int                  warnErr;
0082     int                  warnKOrg;
0083 
0084     explicit UpdateStatusData(KAlarm::UpdateStatus s = KAlarm::UPDATE_OK) : status(s), warnErr(0), warnKOrg(0) {}
0085     int  failedCount() const      { return status.failed.count(); }
0086     void appendFailed(int index)  { status.failed.append(index); }
0087     // Set an error status and increment to number of errors to warn about
0088     void setError(KAlarm::UpdateStatus st, int errorCount = -1, const QString& msg = QString())
0089     {
0090         status.set(st, msg);
0091         if (errorCount < 0)
0092             ++warnErr;
0093         else
0094             warnErr = errorCount;
0095     }
0096     // Set an error status and increment to number of errors to warn about,
0097     // without changing the error message
0098     void updateError(KAlarm::UpdateStatus st, int errorCount = -1)
0099     {
0100         status.status = st;
0101         if (errorCount < 0)
0102             ++warnErr;
0103         else
0104             warnErr = errorCount;
0105     }
0106     // Update the error status with a KOrganizer related status
0107     void korgUpdate(const KAlarm::UpdateResult& result)
0108     {
0109         if (result.status != KAlarm::UPDATE_OK)
0110         {
0111             ++warnKOrg;
0112             if (result.status > status.status)
0113                 status = result;
0114         }
0115     }
0116 };
0117 
0118 const QLatin1String KORG_DBUS_SERVICE("org.kde.korganizer");
0119 const QLatin1String KORG_DBUS_IFACE("org.kde.korganizer.Korganizer");
0120 // D-Bus object path of KOrganizer's notification interface
0121 #define       KORG_DBUS_PATH            "/Korganizer"
0122 #define       KORG_DBUS_LOAD_PATH       "/korganizer_PimApplication"
0123 const QLatin1String KORG_MIME_TYPE("application/x-vnd.akonadi.calendar.event");
0124 const QLatin1String KORGANIZER_UID("korg-");
0125 
0126 const QLatin1String ALARM_OPTS_FILE("alarmopts");
0127 const QLatin1String DONT_SHOW_ERRORS_GROUP("DontShowErrors");
0128 
0129 const QLatin1String GENERAL_GROUP("General");
0130 
0131 KAlarm::UpdateResult updateEvent(KAEvent&, KAlarm::UpdateError, QWidget* msgParent, bool saveIfReadOnly);
0132 void editNewTemplate(EditAlarmDlg::Type, const KAEvent& preset, QWidget* parent);
0133 void displayUpdateError(QWidget* parent, KAlarm::UpdateError, const UpdateStatusData&, bool showKOrgError = true);
0134 KAlarm::UpdateResult sendToKOrganizer(const KAEvent&);
0135 KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID);
0136 KAlarm::UpdateResult runKOrganizer();
0137 QString uidKOrganizer(const QString& eventID);
0138 }
0139 
0140 
0141 namespace KAlarm
0142 {
0143 
0144 // Collect these widget labels together to ensure consistent wording and
0145 // translations across different modules.
0146 QString i18n_act_StopPlay()    { return i18nc("@action", "Stop Play"); }
0147 
0148 Private* Private::mInstance = nullptr;
0149 
0150 /******************************************************************************
0151 * Display a main window with the specified event selected.
0152 */
0153 MainWindow* displayMainWindowSelected(const QString& eventId)
0154 {
0155     MainWindow* win = MainWindow::firstWindow();
0156     if (!win)
0157     {
0158         if (theApp()->checkCalendar())    // ensure calendar is open
0159         {
0160             win = MainWindow::create();
0161             win->show();
0162         }
0163     }
0164     else
0165     {
0166         // There is already a main window, so make it the active window
0167 #if ENABLE_X11
0168         KWindowInfo wi(win->winId(), NET::WMDesktop);
0169         if (!wi.valid()  ||  !wi.isOnDesktop(KX11Extras::currentDesktop()))
0170         {
0171             // The main window isn't on the current virtual desktop. Hide it
0172             // first so that it will be shown on the current desktop when it is
0173             // shown again. Note that this shifts the window's position, so
0174             // don't hide it if it's already on the current desktop.
0175             win->hide();
0176         }
0177 #endif
0178         win->setWindowState(win->windowState() & ~Qt::WindowMinimized);
0179         win->show();
0180         win->raise();
0181         KWindowSystem::activateWindow(win->windowHandle());
0182     }
0183     if (win)
0184         win->selectEvent(eventId);
0185     return win;
0186 }
0187 
0188 /******************************************************************************
0189 * Create an "Alarms Enabled/Enable Alarms" action.
0190 */
0191 KToggleAction* createAlarmEnableAction(QObject* parent)
0192 {
0193     KToggleAction* action = new KToggleAction(i18nc("@action", "Enable Alarms"), parent);
0194     action->setChecked(theApp()->alarmsEnabled());
0195     QObject::connect(action, &QAction::toggled, theApp(), &KAlarmApp::setAlarmsEnabled);
0196     // The following line ensures that all instances are kept in the same state
0197     QObject::connect(theApp(), &KAlarmApp::alarmEnabledToggled, action, &QAction::setChecked);
0198     return action;
0199 }
0200 
0201 /******************************************************************************
0202 * Create a "Stop Play" action.
0203 */
0204 QAction* createStopPlayAction(QObject* parent)
0205 {
0206     QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18n_act_StopPlay(), parent);
0207     action->setEnabled(MessageDisplay::isAudioPlaying());
0208     QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::stopAudio);
0209     // The following line ensures that all instances are kept in the same state
0210     QObject::connect(theApp(), &KAlarmApp::audioPlaying, action, &QAction::setEnabled);
0211     return action;
0212 }
0213 
0214 /******************************************************************************
0215 * Create a "Spread Windows" action.
0216 */
0217 KToggleAction* createSpreadWindowsAction(QObject* parent)
0218 {
0219     KToggleAction* action = new KToggleAction(i18nc("@action", "Spread Windows"), parent);
0220     QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::spreadWindows);
0221     // The following line ensures that all instances are kept in the same state
0222     QObject::connect(theApp(), &KAlarmApp::spreadWindowsToggled, action, &QAction::setChecked);
0223     return action;
0224 }
0225 
0226 /******************************************************************************
0227 * Add a new active (non-archived) alarm, and save it in its resource.
0228 * Parameters: msgParent = parent widget for any calendar selection prompt or
0229 *                         error message.
0230 *             event - is updated with the actual event ID.
0231 */
0232 UpdateResult addEvent(KAEvent& event, Resource& resource, QWidget* msgParent, int options, bool showKOrgErr)
0233 {
0234     qCDebug(KALARM_LOG) << "KAlarm::addEvent:" << event.id();
0235     bool cancelled = false;
0236     UpdateStatusData status;
0237     if (!theApp()->checkCalendar())    // ensure calendar is open
0238         status.status = UPDATE_FAILED;
0239     else
0240     {
0241         // Save the event details in the calendar file, and get the new event ID
0242         // Note that ResourcesCalendar::addEvent() updates 'event'.
0243         ResourcesCalendar::AddEventOptions rc_options = {};
0244         if (options & USE_EVENT_ID)
0245             rc_options |= ResourcesCalendar::UseEventId;
0246         if (options & NO_RESOURCE_PROMPT)
0247             rc_options |= ResourcesCalendar::NoResourcePrompt;
0248         if (!ResourcesCalendar::addEvent(event, resource, msgParent, rc_options, &cancelled))
0249         {
0250             status.status = UPDATE_FAILED;
0251         }
0252         else
0253         {
0254             if (!resource.save(&status.status.message))
0255             {
0256                 resource.reload(true);   // discard the new event
0257                 status.status.status = SAVE_FAILED;
0258             }
0259         }
0260         if (status.status == UPDATE_OK)
0261         {
0262             if ((options & ALLOW_KORG_UPDATE)  &&  event.copyToKOrganizer())
0263             {
0264                 UpdateResult st = sendToKOrganizer(event);    // tell KOrganizer to show the event
0265                 status.korgUpdate(st);
0266             }
0267         }
0268     }
0269 
0270     if (status.status != UPDATE_OK  &&  !cancelled  &&  msgParent)
0271         displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr);
0272     return status.status;
0273 }
0274 
0275 /******************************************************************************
0276 * Add a list of new active (non-archived) alarms, and save them in the resource.
0277 * The events are updated with their actual event IDs.
0278 */
0279 UpdateResult addEvents(QList<KAEvent>& events, Resource& resource, QWidget* msgParent,
0280                        bool allowKOrgUpdate, bool showKOrgErr)
0281 {
0282     qCDebug(KALARM_LOG) << "KAlarm::addEvents:" << events.count();
0283     if (events.isEmpty())
0284         return UpdateResult(UPDATE_OK);
0285     UpdateStatusData status;
0286     if (!theApp()->checkCalendar())    // ensure calendar is open
0287         status.status = UPDATE_FAILED;
0288     else
0289     {
0290         if (!resource.isValid())
0291             resource = Resources::destination(CalEvent::ACTIVE, msgParent);
0292         if (!resource.isValid())
0293         {
0294             qCDebug(KALARM_LOG) << "KAlarm::addEvents: No calendar";
0295             status.status = UPDATE_FAILED;
0296         }
0297         else
0298         {
0299             for (int i = 0, end = events.count();  i < end;  ++i)
0300             {
0301                 // Save the event details in the calendar file, and get the new event ID
0302                 KAEvent& event = events[i];
0303                 if (!ResourcesCalendar::addEvent(event, resource, msgParent))
0304                 {
0305                     status.appendFailed(i);
0306                     status.setError(UPDATE_ERROR);
0307                     continue;
0308                 }
0309                 if (allowKOrgUpdate  &&  event.copyToKOrganizer())
0310                 {
0311                     UpdateResult st = sendToKOrganizer(event);    // tell KOrganizer to show the event
0312                     status.korgUpdate(st);
0313                 }
0314 
0315             }
0316             if (status.failedCount() == events.count())
0317                 status.status = UPDATE_FAILED;
0318             else if (!resource.save(&status.status.message))
0319             {
0320                 resource.reload(true);   // discard the new events
0321                 status.updateError(SAVE_FAILED, events.count());  // everything failed
0322             }
0323         }
0324     }
0325 
0326     if (status.status != UPDATE_OK  &&  msgParent)
0327         displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr);
0328     return status.status;
0329 }
0330 
0331 /******************************************************************************
0332 * Save the event in the archived calendar.
0333 * The event's ID is changed to an archived ID if necessary.
0334 */
0335 bool addArchivedEvent(KAEvent& event, Resource& resource)
0336 {
0337     qCDebug(KALARM_LOG) << "KAlarm::addArchivedEvent:" << event.id();
0338     bool archiving = (event.category() == CalEvent::ACTIVE);
0339     if (archiving  &&  !Preferences::archivedKeepDays())
0340         return false;   // expired alarms aren't being kept
0341     KAEvent newevent(event);
0342     KAEvent* const newev = &newevent;
0343     if (archiving)
0344     {
0345         newev->setCategory(CalEvent::ARCHIVED);    // this changes the event ID
0346         newev->setCreatedDateTime(KADateTime::currentUtcDateTime());   // time stamp to control purging
0347     }
0348     // Add the event from the archived resource. It's not too important whether
0349     // the resource is saved successfully after the deletion, so allow it to be
0350     // saved automatically.
0351     if (!ResourcesCalendar::addEvent(newevent, resource))
0352         return false;
0353     event = *newev;   // update event ID etc.
0354 
0355     return true;
0356 }
0357 
0358 /******************************************************************************
0359 * Add a new template.
0360 * 'event' is updated with the actual event ID.
0361 * Parameters: promptParent = parent widget for any calendar selection prompt.
0362 */
0363 UpdateResult addTemplate(KAEvent& event, Resource& resource, QWidget* msgParent)
0364 {
0365     qCDebug(KALARM_LOG) << "KAlarm::addTemplate:" << event.id();
0366     UpdateStatusData status;
0367 
0368     // Add the template to the calendar file
0369     KAEvent newev(event);
0370     if (!ResourcesCalendar::addEvent(newev, resource, msgParent))
0371         status.status = UPDATE_FAILED;
0372     else
0373     {
0374         event = newev;   // update event ID etc.
0375         if (!resource.save(&status.status.message))
0376         {
0377             resource.reload(true);   // discard the new template
0378             status.status.status = SAVE_FAILED;
0379         }
0380         else
0381             return UpdateResult(UPDATE_OK);
0382     }
0383 
0384     if (msgParent)
0385         displayUpdateError(msgParent, ERR_TEMPLATE, status);
0386     return status.status;
0387 }
0388 
0389 /******************************************************************************
0390 * Modify an active (non-archived) alarm in a resource.
0391 * The new event must have a different event ID from the old one.
0392 */
0393 UpdateResult modifyEvent(KAEvent& oldEvent, KAEvent& newEvent, QWidget* msgParent, bool showKOrgErr)
0394 {
0395     qCDebug(KALARM_LOG) << "KAlarm::modifyEvent:" << oldEvent.id();
0396 
0397     UpdateStatusData status;
0398     Resource resource = Resources::resource(oldEvent.resourceId());
0399     if (!newEvent.isValid())
0400     {
0401         deleteEvent(oldEvent, resource, true);
0402         status.status = UPDATE_FAILED;
0403     }
0404     else
0405     {
0406         EventId oldId(oldEvent);
0407         if (oldEvent.copyToKOrganizer())
0408         {
0409             // Tell KOrganizer to delete its old event.
0410             // But ignore errors, because the user could have manually
0411             // deleted it since KAlarm asked KOrganizer to set it up.
0412             deleteFromKOrganizer(oldId.eventId());
0413         }
0414         // Update the event in the calendar file, and get the new event ID
0415         if (!ResourcesCalendar::modifyEvent(oldId, newEvent))
0416             status.status = UPDATE_FAILED;
0417         else
0418         {
0419             if (!resource.save(&status.status.message))
0420             {
0421                 resource.reload(true);   // retrieve the pre-update version of the event
0422                 status.status.status = SAVE_FAILED;
0423             }
0424             if (status.status == UPDATE_OK)
0425             {
0426                 if (newEvent.copyToKOrganizer())
0427                 {
0428                     UpdateResult st = sendToKOrganizer(newEvent);    // tell KOrganizer to show the new event
0429                     status.korgUpdate(st);
0430                 }
0431 
0432                 // Remove "Don't show error messages again" for the old alarm
0433                 setDontShowErrors(oldId);
0434             }
0435         }
0436     }
0437 
0438     if (status.status != UPDATE_OK  &&  msgParent)
0439         displayUpdateError(msgParent, ERR_MODIFY, status, showKOrgErr);
0440     return status.status;
0441 }
0442 
0443 /******************************************************************************
0444 * Update an active (non-archived) alarm in its resource.
0445 * The new event will have the same event ID as the old one.
0446 * The event is not updated in KOrganizer, since this function is called when an
0447 * existing alarm is rescheduled (due to recurrence or deferral).
0448 */
0449 UpdateResult updateEvent(KAEvent& event, QWidget* msgParent, bool archiveOnDelete, bool saveIfReadOnly)
0450 {
0451     qCDebug(KALARM_LOG) << "KAlarm::updateEvent:" << event.id();
0452 
0453     if (!event.isValid())
0454     {
0455         Resource resource;
0456         deleteEvent(event, resource, archiveOnDelete);
0457         return UpdateResult(UPDATE_OK);
0458     }
0459 
0460     // Update the event in the resource.
0461     return ::updateEvent(event, ERR_MODIFY, msgParent, saveIfReadOnly);
0462 }
0463 
0464 /******************************************************************************
0465 * Update a template in its resource.
0466 */
0467 UpdateResult updateTemplate(KAEvent& event, QWidget* msgParent)
0468 {
0469     return ::updateEvent(event, ERR_TEMPLATE, msgParent, true);
0470 }
0471 
0472 /******************************************************************************
0473 * Delete alarms from a resource.
0474 * If the events are archived, the events' IDs are changed to archived IDs if necessary.
0475 */
0476 UpdateResult deleteEvent(KAEvent& event, Resource& resource, bool archive, QWidget* msgParent, bool showKOrgErr)
0477 {
0478     QList<KAEvent> events(1, event);
0479     return deleteEvents(events, resource, archive, msgParent, showKOrgErr);
0480 }
0481 
0482 UpdateResult deleteEvents(QList<KAEvent>& events, Resource& resource, bool archive, QWidget* msgParent, bool showKOrgErr)
0483 {
0484     qCDebug(KALARM_LOG) << "KAlarm::deleteEvents:" << events.count();
0485     if (events.isEmpty())
0486         return UpdateResult(UPDATE_OK);
0487     UpdateStatusData status;
0488 #if ENABLE_RTC_WAKE_FROM_SUSPEND
0489     bool deleteWakeFromSuspendAlarm = false;
0490     const QString wakeFromSuspendId = checkRtcWakeConfig().value(0);
0491 #endif
0492     QSet<Resource> resources;   // resources which events have been deleted from
0493     for (int i = 0, end = events.count();  i < end;  ++i)
0494     {
0495         // Save the event details in the calendar file, and get the new event ID
0496         KAEvent* event = &events[i];
0497         const QString id = event->id();
0498 
0499         // Delete the event from the calendar file
0500         if (event->category() != CalEvent::ARCHIVED)
0501         {
0502             if (event->copyToKOrganizer())
0503             {
0504                 // The event was shown in KOrganizer, so tell KOrganizer to
0505                 // delete it. But ignore errors, because the user could have
0506                 // manually deleted it from KOrganizer since it was set up.
0507                 UpdateResult st = deleteFromKOrganizer(id);
0508                 status.korgUpdate(st);
0509             }
0510             if (archive  &&  event->toBeArchived())
0511             {
0512                 Resource archResource;
0513                 KAEvent ev(*event);
0514                 addArchivedEvent(ev, archResource);  // this changes the event ID to an archived ID
0515             }
0516         }
0517         Resource res = resource;
0518         if (ResourcesCalendar::deleteEvent(*event, res, false))   // don't save calendar after deleting
0519             resources.insert(res);
0520         else
0521             status.appendFailed(i);
0522 
0523 #if ENABLE_RTC_WAKE_FROM_SUSPEND
0524         if (id == wakeFromSuspendId)
0525             deleteWakeFromSuspendAlarm = true;
0526 #endif
0527 
0528         // Remove "Don't show error messages again" for this alarm
0529         setDontShowErrors(EventId(*event));
0530     }
0531 
0532     if (!resource.isValid()  &&  resources.size() == 1)
0533         resource = *resources.constBegin();
0534 
0535     if (status.failedCount())
0536         status.setError(status.failedCount() == events.count() ? UPDATE_FAILED : UPDATE_ERROR, status.failedCount());
0537     if (status.failedCount() < events.count())
0538     {
0539         QString msg;
0540         for (Resource res : resources)
0541             if (!res.save(&msg))
0542             {
0543                 res.reload(true);    // retrieve the events which couldn't be deleted
0544                 status.setError(SAVE_FAILED, status.failedCount(), msg);
0545             }
0546     }
0547     if (status.status != UPDATE_OK  &&  msgParent)
0548         displayUpdateError(msgParent, ERR_DELETE, status, showKOrgErr);
0549 
0550 #if ENABLE_RTC_WAKE_FROM_SUSPEND
0551     // Remove any wake-from-suspend scheduled for a deleted alarm
0552     if (deleteWakeFromSuspendAlarm  &&  !wakeFromSuspendId.isEmpty())
0553         cancelRtcWake(msgParent, wakeFromSuspendId);
0554 #endif
0555 
0556     return status.status;
0557 }
0558 
0559 /******************************************************************************
0560 * Delete templates from the calendar file.
0561 */
0562 UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent)
0563 {
0564     int count = events.count();
0565     qCDebug(KALARM_LOG) << "KAlarm::deleteTemplates:" << count;
0566     if (!count)
0567         return UpdateResult(UPDATE_OK);
0568     UpdateStatusData status;
0569     QSet<Resource> resources;   // resources which events have been deleted from
0570     for (int i = 0, end = events.count();  i < end;  ++i)
0571     {
0572         // Update the window lists
0573         // Delete the template from the calendar file
0574         const KAEvent* event = events[i];
0575         Resource resource;
0576         if (ResourcesCalendar::deleteEvent(*event, resource, false))   // don't save calendar after deleting
0577             resources.insert(resource);
0578         else
0579             status.appendFailed(i);
0580     }
0581 
0582     if (status.failedCount())
0583         status.setError(status.failedCount() == events.count() ? UPDATE_FAILED : UPDATE_ERROR, status.failedCount());
0584     if (status.failedCount() < events.count())
0585     {
0586         QString msg;
0587         for (Resource res : resources)
0588             if (!res.save(&msg))
0589             {
0590                 res.reload(true);    // retrieve the templates which couldn't be deleted
0591                 status.status.message = msg;
0592                 status.setError(SAVE_FAILED, status.failedCount(), msg);
0593             }
0594     }
0595 
0596     if (status.status != UPDATE_OK  &&  msgParent)
0597         displayUpdateError(msgParent, ERR_TEMPLATE, status);
0598     return status.status;
0599 }
0600 
0601 /******************************************************************************
0602 * Delete an alarm from the display calendar.
0603 */
0604 void deleteDisplayEvent(const QString& eventID)
0605 {
0606     qCDebug(KALARM_LOG) << "KAlarm::deleteDisplayEvent:" << eventID;
0607     if (DisplayCalendar::open())
0608         DisplayCalendar::deleteEvent(eventID, true);   // save calendar after deleting
0609 }
0610 
0611 /******************************************************************************
0612 * Undelete archived alarms.
0613 * The archive bit is set to ensure that they get re-archived if deleted again.
0614 * Parameters:
0615 *   calendar - the active alarms calendar to restore the alarms into, or null
0616 *              to use the default way of determining the active alarm calendar.
0617 *   ineligibleIndexes - will be filled in with the indexes to any ineligible events.
0618 */
0619 UpdateResult reactivateEvent(KAEvent& event, Resource& resource, QWidget* msgParent, bool showKOrgErr)
0620 {
0621     QList<int> ids;
0622     QList<KAEvent> events(1, event);
0623     return reactivateEvents(events, ids, resource, msgParent, showKOrgErr);
0624 }
0625 
0626 UpdateResult reactivateEvents(QList<KAEvent>& events, QList<int>& ineligibleIndexes,
0627                               Resource& resource, QWidget* msgParent, bool showKOrgErr)
0628 {
0629     qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents:" << events.count();
0630     ineligibleIndexes.clear();
0631     if (events.isEmpty())
0632         return UpdateResult(UPDATE_OK);
0633     UpdateStatusData status;
0634     if (!resource.isValid())
0635         resource = Resources::destination(CalEvent::ACTIVE, msgParent);
0636     if (!resource.isValid())
0637     {
0638         qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents: No calendar";
0639         status.setError(UPDATE_FAILED, events.count());
0640     }
0641     else
0642     {
0643         int count = 0;
0644         const KADateTime now = KADateTime::currentUtcDateTime();
0645         QList<KAEvent> eventsToDelete;
0646         for (int i = 0, end = events.count();  i < end;  ++i)
0647         {
0648             // Delete the event from the archived resource
0649             KAEvent* event = &events[i];
0650             if (event->category() != CalEvent::ARCHIVED
0651             ||  !event->occursAfter(now, true))
0652             {
0653                 ineligibleIndexes += i;
0654                 continue;
0655             }
0656             ++count;
0657 
0658             KAEvent newevent(*event);
0659             KAEvent* const newev = &newevent;
0660             newev->setCategory(CalEvent::ACTIVE);    // this changes the event ID
0661             if (newev->recurs()  ||  newev->repetition())
0662                 newev->setNextOccurrence(now);   // skip any recurrences in the past
0663             newev->setArchive();    // ensure that it gets re-archived if it is deleted
0664 
0665             // Save the event details in the calendar file.
0666             // This converts the event ID.
0667             if (!ResourcesCalendar::addEvent(newevent, resource, msgParent, ResourcesCalendar::UseEventId))
0668             {
0669                 status.appendFailed(i);
0670                 continue;
0671             }
0672             if (newev->copyToKOrganizer())
0673             {
0674                 UpdateResult st = sendToKOrganizer(*newev);    // tell KOrganizer to show the event
0675                 status.korgUpdate(st);
0676             }
0677 
0678             if (ResourcesCalendar::event(EventId(*event)).isValid())  // no error if event doesn't exist in archived resource
0679                 eventsToDelete.append(*event);
0680             events[i] = newevent;
0681         }
0682 
0683         if (status.failedCount())
0684             status.setError(status.failedCount() == events.count() ? UPDATE_FAILED : UPDATE_ERROR, status.failedCount());
0685         if (status.failedCount() < events.count())
0686         {
0687             if (!resource.save(&status.status.message))
0688             {
0689                 resource.reload(true);
0690                 status.updateError(SAVE_FAILED, count);
0691             }
0692             else
0693             {
0694                 // Delete the events from the archived resources. It's not too
0695                 // important whether the resources are saved successfully after
0696                 // the deletion, so allow them to be saved automatically.
0697                 for (const KAEvent& event : eventsToDelete)
0698                 {
0699                     Resource res = Resources::resource(event.resourceId());
0700                     if (!ResourcesCalendar::deleteEvent(event, res, false))
0701                         status.setError(UPDATE_ERROR);
0702                 }
0703             }
0704         }
0705     }
0706     if (status.status != UPDATE_OK  &&  msgParent)
0707         displayUpdateError(msgParent, ERR_REACTIVATE, status, showKOrgErr);
0708     return status.status;
0709 }
0710 
0711 /******************************************************************************
0712 * Enable or disable alarms.
0713 * The new events will have the same event IDs as the old ones.
0714 */
0715 UpdateResult enableEvents(QList<KAEvent>& events, bool enable, QWidget* msgParent)
0716 {
0717     qCDebug(KALARM_LOG) << "KAlarm::enableEvents:" << events.count();
0718     if (events.isEmpty())
0719         return UpdateResult(UPDATE_OK);
0720     UpdateStatusData status;
0721 #if ENABLE_RTC_WAKE_FROM_SUSPEND
0722     bool deleteWakeFromSuspendAlarm = false;
0723     const QString wakeFromSuspendId = checkRtcWakeConfig().value(0);
0724 #endif
0725     QSet<ResourceId> resourceIds;   // resources whose events have been updated
0726     for (int i = 0, end = events.count();  i < end;  ++i)
0727     {
0728         KAEvent* event = &events[i];
0729         if (event->category() == CalEvent::ACTIVE
0730         &&  enable != event->enabled())
0731         {
0732             event->setEnabled(enable);
0733 
0734 #if ENABLE_RTC_WAKE_FROM_SUSPEND
0735             if (!enable  &&  event->id() == wakeFromSuspendId)
0736                 deleteWakeFromSuspendAlarm = true;
0737 #endif
0738 
0739             // Update the event in the calendar file
0740             const KAEvent newev = ResourcesCalendar::updateEvent(*event);
0741             if (!newev.isValid())
0742             {
0743                 qCCritical(KALARM_LOG) << "KAlarm::enableEvents: Error updating event in calendar:" << event->id();
0744                 status.appendFailed(i);
0745             }
0746             else
0747             {
0748                 resourceIds.insert(event->resourceId());
0749                 ResourcesCalendar::disabledChanged(newev);
0750 
0751                 // If we're disabling a display alarm, close any message display
0752                 if (!enable  &&  (event->actionTypes() & KAEvent::Action::Display))
0753                 {
0754                     MessageDisplay* win = MessageDisplay::findEvent(EventId(*event));
0755                     delete win;
0756                 }
0757             }
0758         }
0759     }
0760 
0761     if (status.failedCount())
0762         status.setError(status.failedCount() == events.count() ? UPDATE_FAILED : UPDATE_ERROR, status.failedCount());
0763     if (status.failedCount() < events.count())
0764     {
0765         QString msg;
0766         for (ResourceId id : resourceIds)
0767         {
0768             Resource res = Resources::resource(id);
0769             if (!res.save(&msg))
0770             {
0771                 // Don't reload resource after failed save. It's better to
0772                 // keep the new enabled status of the alarms at least until
0773                 // KAlarm is restarted.
0774                 status.setError(SAVE_FAILED, status.failedCount(), msg);
0775             }
0776         }
0777     }
0778     if (status.status != UPDATE_OK  &&  msgParent)
0779         displayUpdateError(msgParent, ERR_ADD, status);
0780 
0781 #if ENABLE_RTC_WAKE_FROM_SUSPEND
0782     // Remove any wake-from-suspend scheduled for a disabled alarm
0783     if (deleteWakeFromSuspendAlarm  &&  !wakeFromSuspendId.isEmpty())
0784         cancelRtcWake(msgParent, wakeFromSuspendId);
0785 #endif
0786 
0787     return status.status;
0788 }
0789 
0790 /******************************************************************************
0791 * This method must only be called from the main KAlarm queue processing loop,
0792 * to prevent asynchronous calendar operations interfering with one another.
0793 *
0794 * Purge all archived events from the default archived alarm resource whose end
0795 * time is longer ago than 'purgeDays'. All events are deleted if 'purgeDays' is
0796 * zero.
0797 */
0798 void purgeArchive(int purgeDays)
0799 {
0800     if (purgeDays < 0)
0801         return;
0802     qCDebug(KALARM_LOG) << "KAlarm::purgeArchive:" << purgeDays;
0803     const QDate cutoff = KADateTime::currentLocalDate().addDays(-purgeDays);
0804     const Resource resource = Resources::getStandard(CalEvent::ARCHIVED, true);
0805     if (!resource.isValid())
0806         return;
0807     QList<KAEvent> events = ResourcesCalendar::events(resource);
0808     for (int i = 0;  i < events.count();  )
0809     {
0810         if (purgeDays  &&  events.at(i).createdDateTime().date() >= cutoff)
0811             events.remove(i);
0812         else
0813             ++i;
0814     }
0815     if (!events.isEmpty())
0816         ResourcesCalendar::purgeEvents(events);   // delete the events and save the calendar
0817 }
0818 
0819 /******************************************************************************
0820 * Return whether an event is read-only.
0821 */
0822 bool eventReadOnly(const QString& eventId)
0823 {
0824     KAEvent event;
0825     const Resource resource = Resources::resourceForEvent(eventId, event);
0826     return !event.isValid()  ||  event.isReadOnly()
0827        ||  !resource.isWritable(event.category());
0828 }
0829 
0830 /******************************************************************************
0831 * Display an error message about an error when saving an event.
0832 * If 'model' is non-null, the AlarmListModel* which it points to is used; if
0833 * that is null, it is created.
0834 */
0835 QList<KAEvent> getSortedActiveEvents(QObject* parent, AlarmListModel** model)
0836 {
0837     AlarmListModel* mdl = nullptr;
0838     if (!model)
0839         model = &mdl;
0840     if (!*model)
0841     {
0842         *model = DataModel::createAlarmListModel(parent);
0843         (*model)->setEventTypeFilter(CalEvent::ACTIVE);
0844         (*model)->sort(AlarmListModel::TimeColumn);
0845     }
0846     int count = (*model)->rowCount();
0847     QList<KAEvent> result;
0848     result.reserve(count);
0849     for (int i = 0;  i < count;  ++i)
0850     {
0851         const KAEvent event = (*model)->event(i);
0852         if (event.enabled()  &&  !event.expired())
0853             result += event;
0854     }
0855     return result;
0856 }
0857 
0858 /******************************************************************************
0859 * Import alarms from an external calendar and merge them into KAlarm's calendar.
0860 * The alarms are given new unique event IDs.
0861 * Parameters: parent = parent widget for error message boxes
0862 * Reply = true if all alarms in the calendar were successfully imported
0863 *       = false if any alarms failed to be imported.
0864 */
0865 bool importAlarms(Resource& resource, QWidget* parent)
0866 {
0867     qCDebug(KALARM_LOG) << "KAlarm::importAlarms" << resource.displayId();
0868     // Use KFileCustomDialog to allow files' mime types to be determined by
0869     // both file name and content, instead of QFileDialog which only looks at
0870     // files' names. This is needed in particular when importing an old KAlarm
0871     // calendar directory, in order to list the calendar files within it, since
0872     // each calendar file name is simply the UID of the event within it, without
0873     // a .ics extension.
0874     AutoQPointer<KFileCustomDialog> dlg = new KFileCustomDialog(lastImportUrl, parent);
0875     dlg->setWindowTitle(i18nc("@title:window", "Import Calendar Files"));
0876     KFileWidget* widget = dlg->fileWidget();
0877     widget->setOperationMode(KFileWidget::Opening);
0878     widget->setMode(KFile::Files | KFile::ExistingOnly);
0879     widget->setFilters({KFileFilter::fromMimeType(QStringLiteral("text/calendar"))});
0880     dlg->setWindowModality(Qt::WindowModal);
0881     dlg->exec();
0882     if (!dlg)
0883         return false;
0884     const QList<QUrl> urls = widget->selectedUrls();
0885     if (urls.isEmpty())
0886         return false;
0887     lastImportUrl = urls[0].adjusted(QUrl::RemoveFilename);
0888 
0889     const CalEvent::Types alarmTypes = resource.isValid() ? resource.alarmTypes() : CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE;
0890 
0891     // Read all the selected calendar files and extract their alarms.
0892     QHash<CalEvent::Type, QList<KAEvent>> events;
0893     for (const QUrl& url : urls)
0894     {
0895         if (!url.isValid())
0896         {
0897             qCDebug(KALARM_LOG) << "KAlarm::importAlarms: Invalid URL";
0898             continue;
0899         }
0900         qCDebug(KALARM_LOG) << "KAlarm::importAlarms:" << url.toDisplayString();
0901         importCalendarFile(url, alarmTypes, true, parent, events);
0902     }
0903     if (events.isEmpty())
0904         return false;
0905 
0906     // Add the alarms to the destination resource.
0907     bool success = true;
0908     for (auto it = events.constBegin();  it != events.constEnd();  ++it)
0909     {
0910         Resource res;
0911         if (resource.isValid())
0912             res = resource;
0913         else
0914             res = Resources::destination(it.key());
0915 
0916         for (const KAEvent& event : std::as_const(it.value()))
0917         {
0918             if (!res.addEvent(event))
0919                 success = false;
0920         }
0921     }
0922     return success;
0923 }
0924 
0925 /******************************************************************************
0926 * Export all selected alarms to an external calendar.
0927 * The alarms are given new unique event IDs.
0928 * Parameters: parent = parent widget for error message boxes
0929 * Reply = true if all alarms in the calendar were successfully exported
0930 *       = false if any alarms failed to be exported.
0931 */
0932 bool exportAlarms(const QList<KAEvent>& events, QWidget* parent)
0933 {
0934     bool append;
0935     QString file = FileDialog::getSaveFileName(lastExportUrl,
0936                                                {KFileFilter(i18nc("@item:inlistbox File type selection filter", "Calendar Files"), {QStringLiteral("*.ics")}, {})},
0937                                                parent, i18nc("@title:window", "Choose Export Calendar"),
0938                                                &append);
0939     if (file.isEmpty())
0940         return false;
0941     const QUrl url = QUrl::fromLocalFile(file);
0942     if (!url.isValid())
0943     {
0944         qCDebug(KALARM_LOG) << "KAlarm::exportAlarms: Invalid URL" << url;
0945         return false;
0946     }
0947     lastExportUrl = url.adjusted(QUrl::RemoveFilename);
0948     qCDebug(KALARM_LOG) << "KAlarm::exportAlarms:" << url.toDisplayString();
0949 
0950     MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone()));
0951     FileStorage::Ptr calStorage(new FileStorage(calendar, file));
0952     if (append  &&  !calStorage->load())
0953     {
0954         auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatDetail::StatDefaultDetails);
0955         KJobWidgets::setWindow(statJob, parent);
0956         statJob->exec();
0957         KFileItem fi(statJob->statResult(), url);
0958         if (fi.size())
0959         {
0960             qCCritical(KALARM_LOG) << "KAlarm::exportAlarms: Error loading calendar file" << file << "for append";
0961             KAMessageBox::error(MainWindow::mainMainWindow(),
0962                                 xi18nc("@info", "Error loading calendar to append to:<nl/><filename>%1</filename>", url.toDisplayString()));
0963             return false;
0964         }
0965     }
0966     KACalendar::setKAlarmVersion(calendar);
0967 
0968     // Add the alarms to the calendar
0969     bool success = true;
0970     bool exported = false;
0971     for (const KAEvent& event : events)
0972     {
0973         Event::Ptr kcalEvent(new Event);
0974         const CalEvent::Type type = event.category();
0975         const QString id = CalEvent::uid(kcalEvent->uid(), type);
0976         kcalEvent->setUid(id);
0977         event.updateKCalEvent(kcalEvent, KAEvent::UidAction::Ignore);
0978         if (calendar->addEvent(kcalEvent))
0979             exported = true;
0980         else
0981             success = false;
0982     }
0983 
0984     if (exported)
0985     {
0986         // One or more alarms have been exported to the calendar.
0987         // Save the calendar to file.
0988         QTemporaryFile* tempFile = nullptr;
0989         bool local = url.isLocalFile();
0990         if (!local)
0991         {
0992             tempFile = new QTemporaryFile;
0993             file = tempFile->fileName();
0994         }
0995         calStorage->setFileName(file);
0996         calStorage->setSaveFormat(new ICalFormat);
0997         if (!calStorage->save())
0998         {
0999             qCCritical(KALARM_LOG) << "KAlarm::exportAlarms:" << file << ": failed";
1000             KAMessageBox::error(MainWindow::mainMainWindow(),
1001                                 xi18nc("@info", "Failed to save new calendar to:<nl/><filename>%1</filename>", url.toDisplayString()));
1002             success = false;
1003         }
1004         else if (!local)
1005         {
1006             QFile qFile(file);
1007             qFile.open(QIODevice::ReadOnly);
1008             auto uploadJob = KIO::storedPut(&qFile, url, -1);
1009             KJobWidgets::setWindow(uploadJob, parent);
1010             if (!uploadJob->exec())
1011             {
1012                 qCCritical(KALARM_LOG) << "KAlarm::exportAlarms:" << file << ": upload failed";
1013                 KAMessageBox::error(MainWindow::mainMainWindow(),
1014                                     xi18nc("@info", "Cannot upload new calendar to:<nl/><filename>%1</filename>", url.toDisplayString()));
1015                 success = false;
1016             }
1017         }
1018         delete tempFile;
1019     }
1020     return success;
1021 }
1022 
1023 /******************************************************************************
1024 * Display an error message corresponding to a specified alarm update error code.
1025 */
1026 void displayKOrgUpdateError(QWidget* parent, UpdateError code, const UpdateResult& korgError, int nAlarms)
1027 {
1028     QString errmsg;
1029     switch (code)
1030     {
1031         case ERR_ADD:
1032         case ERR_REACTIVATE:
1033             errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to show alarms in KOrganizer")
1034                                    : i18nc("@info", "Unable to show alarm in KOrganizer");
1035             break;
1036         case ERR_MODIFY:
1037             errmsg = i18nc("@info", "Unable to update alarm in KOrganizer");
1038             break;
1039         case ERR_DELETE:
1040             errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to delete alarms from KOrganizer")
1041                                    : i18nc("@info", "Unable to delete alarm from KOrganizer");
1042             break;
1043         case ERR_TEMPLATE:
1044             return;
1045     }
1046     bool showDetail = !korgError.message.isEmpty();
1047     QString msg;
1048     switch (korgError.status)
1049     {
1050         case UPDATE_KORG_ERRINIT:
1051             msg = xi18nc("@info", "<para>%1</para><para>(Could not start KOrganizer)</para>", errmsg);
1052             break;
1053         case UPDATE_KORG_ERRSTART:
1054             msg = xi18nc("@info", "<para>%1</para><para>(KOrganizer not fully started)</para>", errmsg);
1055             break;
1056         case UPDATE_KORG_ERR:
1057             msg = xi18nc("@info", "<para>%1</para><para>(Error communicating with KOrganizer)</para>", errmsg);
1058             break;
1059         default:
1060             msg = errmsg;
1061             showDetail = false;
1062             break;
1063     }
1064     if (showDetail)
1065         KAMessageBox::detailedError(parent, msg, korgError.message);
1066     else
1067         KAMessageBox::error(parent, msg);
1068 }
1069 
1070 /******************************************************************************
1071 * Execute a New Alarm dialog for the specified alarm type.
1072 */
1073 void editNewAlarm(EditAlarmDlg::Type type, QWidget* parent)
1074 {
1075     editNewAlarm(type, QDate(), parent);
1076 }
1077 void editNewAlarm(EditAlarmDlg::Type type, const QDate& startDate, QWidget* parent)
1078 {
1079     EditAlarmDlg* editDlg = EditAlarmDlg::create(false, type, parent);
1080     if (editDlg)
1081         execNewAlarmDlg(editDlg, startDate);
1082 }
1083 
1084 /******************************************************************************
1085 * Execute a New Alarm dialog for the specified alarm type.
1086 */
1087 void editNewAlarm(KAEvent::SubAction action, QWidget* parent, const AlarmText* text)
1088 {
1089     bool setAction = false;
1090     EditAlarmDlg::Type type;
1091     switch (action)
1092     {
1093         case KAEvent::SubAction::Message:
1094         case KAEvent::SubAction::File:
1095             type = EditAlarmDlg::DISPLAY;
1096             setAction = true;
1097             break;
1098         case KAEvent::SubAction::Command:
1099             type = EditAlarmDlg::COMMAND;
1100             break;
1101         case KAEvent::SubAction::Email:
1102             type = EditAlarmDlg::EMAIL;
1103             break;
1104         case KAEvent::SubAction::Audio:
1105             type = EditAlarmDlg::AUDIO;
1106             break;
1107         default:
1108             return;
1109     }
1110     EditAlarmDlg* editDlg = EditAlarmDlg::create(false, type, parent);
1111     if (!editDlg)
1112         return;
1113     if (setAction  ||  text)
1114         editDlg->setAction(action, *text);
1115     execNewAlarmDlg(editDlg);
1116 }
1117 
1118 /******************************************************************************
1119 * Execute a New Alarm dialog, optionally either presetting it to the supplied
1120 * event, or setting the action and text.
1121 */
1122 void editNewAlarm(const KAEvent& preset, QWidget* parent)
1123 {
1124     editNewAlarm(preset, QDate(), parent);
1125 }
1126 void editNewAlarm(const KAEvent& preset, const QDate& startDate, QWidget* parent)
1127 {
1128     EditAlarmDlg* editDlg = EditAlarmDlg::create(false, preset, true, parent);
1129     if (editDlg)
1130         execNewAlarmDlg(editDlg, startDate);
1131 }
1132 
1133 /******************************************************************************
1134 * Common code for editNewAlarm() variants.
1135 */
1136 void execNewAlarmDlg(EditAlarmDlg* editDlg, const QDate& startDate)
1137 {
1138     if (startDate.isValid())
1139     {
1140         KADateTime defaultTime = editDlg->time();
1141         defaultTime.setDate(startDate);
1142         editDlg->setTime(defaultTime);
1143     }
1144 
1145     // Create a PrivateNewAlarmDlg parented by editDlg.
1146     // It will be deleted when editDlg is closed.
1147     new PrivateNewAlarmDlg(editDlg);
1148     editDlg->show();
1149     editDlg->raise();
1150     editDlg->activateWindow();
1151 }
1152 
1153 PrivateNewAlarmDlg::PrivateNewAlarmDlg(EditAlarmDlg* dlg)
1154     : QObject(dlg)
1155 {
1156     connect(dlg, &QDialog::accepted, this, &PrivateNewAlarmDlg::okClicked);
1157     connect(dlg, &QDialog::rejected, this, &PrivateNewAlarmDlg::cancelClicked);
1158 }
1159 
1160 /******************************************************************************
1161 * Called when the dialogue is accepted (e.g. by clicking the OK button).
1162 * Creates the event specified in the instance's dialogue.
1163 */
1164 void PrivateNewAlarmDlg::okClicked()
1165 {
1166     accept(static_cast<EditAlarmDlg*>(parent()));
1167 }
1168 
1169 /******************************************************************************
1170 * Creates the event specified in a given dialogue.
1171 */
1172 void PrivateNewAlarmDlg::accept(EditAlarmDlg* editDlg)
1173 {
1174     KAEvent event;
1175     Resource resource;
1176     editDlg->getEvent(event, resource);
1177 
1178     // Add the alarm to the displayed lists and to the calendar file
1179     const UpdateResult status = addEvent(event, resource, editDlg);
1180     switch (status.status)
1181     {
1182         case UPDATE_FAILED:
1183         case SAVE_FAILED:
1184             return;
1185         case UPDATE_KORG_ERR:
1186         case UPDATE_KORG_ERRINIT:
1187         case UPDATE_KORG_ERRSTART:
1188         case UPDATE_KORG_FUNCERR:
1189             displayKOrgUpdateError(editDlg, ERR_ADD, status);
1190             break;
1191         default:
1192             break;
1193     }
1194     Undo::saveAdd(event, resource);
1195 
1196     outputAlarmWarnings(editDlg, &event);
1197 
1198     editDlg->deleteLater();
1199 }
1200 
1201 /******************************************************************************
1202 * Called when the dialogue is rejected (e.g. by clicking the Cancel button).
1203 */
1204 void PrivateNewAlarmDlg::cancelClicked()
1205 {
1206     static_cast<EditAlarmDlg*>(parent())->deleteLater();
1207 }
1208 
1209 /******************************************************************************
1210 * Display the alarm edit dialog to edit a new alarm, preset with a template.
1211 */
1212 bool editNewAlarm(const QString& templateName, QWidget* parent)
1213 {
1214     if (!templateName.isEmpty())
1215     {
1216         KAEvent templateEvent = ResourcesCalendar::templateEvent(templateName);
1217         if (templateEvent.isValid())
1218         {
1219             editNewAlarm(templateEvent, parent);
1220             return true;
1221         }
1222         qCWarning(KALARM_LOG) << "KAlarm::editNewAlarm:" << templateName << ": template not found";
1223     }
1224     return false;
1225 }
1226 
1227 /******************************************************************************
1228 * Create a new template.
1229 */
1230 void editNewTemplate(EditAlarmDlg::Type type, QWidget* parent)
1231 {
1232     ::editNewTemplate(type, KAEvent(), parent);
1233 }
1234 
1235 /******************************************************************************
1236 * Create a new template, based on an existing event or template.
1237 */
1238 void editNewTemplate(const KAEvent& preset, QWidget* parent)
1239 {
1240     ::editNewTemplate(EditAlarmDlg::Type(0), preset, parent);
1241 }
1242 
1243 #if ENABLE_RTC_WAKE_FROM_SUSPEND
1244 /******************************************************************************
1245 * Check the config as to whether there is a wake-on-suspend alarm pending, and
1246 * if so, delete it from the config if it has expired.
1247 * If 'checkExists' is true, the config entry will only be returned if the
1248 * event exists.
1249 * Reply = config entry: [0] = event's resource ID,
1250 *                       [1] = event ID,
1251 *                       [2] = trigger time (int64 seconds since epoch).
1252 *       = empty list if none or expired.
1253 */
1254 QStringList checkRtcWakeConfig(bool checkEventExists)
1255 {
1256     KConfigGroup config(KSharedConfig::openConfig(), GENERAL_GROUP);
1257     const QStringList params = config.readEntry("RtcWake", QStringList());
1258     if (params.count() == 3  &&  params[2].toLongLong() > KADateTime::currentUtcDateTime().toSecsSinceEpoch())
1259     {
1260         if (checkEventExists)
1261         {
1262             Resource resource = Resources::resource(params[0].toLongLong());
1263             if (!resource.event(params[1]).isValid())
1264                 return {};
1265         }
1266         return params;                   // config entry is valid
1267     }
1268     if (!params.isEmpty())
1269     {
1270         config.deleteEntry("RtcWake");   // delete the expired config entry
1271         config.sync();
1272     }
1273     return {};
1274 }
1275 
1276 /******************************************************************************
1277 * Delete any wake-on-suspend alarm from the config.
1278 */
1279 void deleteRtcWakeConfig()
1280 {
1281     KConfigGroup config(KSharedConfig::openConfig(), GENERAL_GROUP);
1282     config.deleteEntry("RtcWake");
1283     config.sync();
1284 }
1285 
1286 /******************************************************************************
1287 * Delete any wake-on-suspend alarm, optionally only for a specified event.
1288 */
1289 void cancelRtcWake(QWidget* msgParent, const QString& eventId)
1290 {
1291     const QStringList wakeup = checkRtcWakeConfig();
1292     if (!wakeup.isEmpty()  &&  (eventId.isEmpty() || wakeup[0] == eventId))
1293     {
1294         Private::instance()->mMsgParent = msgParent ? msgParent : MainWindow::mainMainWindow();
1295         QTimer::singleShot(0, Private::instance(), &Private::cancelRtcWake);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1296     }
1297 }
1298 
1299 /******************************************************************************
1300 * Delete any wake-on-suspend alarm.
1301 */
1302 void Private::cancelRtcWake()
1303 {
1304     // setRtcWakeTime will only work with a parent window specified
1305     setRtcWakeTime(0, mMsgParent);
1306     deleteRtcWakeConfig();
1307     KAMessageBox::information(mMsgParent, i18nc("info", "The scheduled Wake from Suspend has been cancelled."));
1308 }
1309 
1310 /******************************************************************************
1311 * Set the wakeup time for the system.
1312 * Set 'triggerTime' to zero to cancel the wakeup.
1313 * Reply = true if successful.
1314 */
1315 bool setRtcWakeTime(unsigned triggerTime, QWidget* parent)
1316 {
1317     QVariantMap args;
1318     args[QStringLiteral("time")] = triggerTime;
1319     KAuth::Action action(QStringLiteral("org.kde.kalarm.rtcwake.settimer"));
1320     action.setHelperId(QStringLiteral("org.kde.kalarm.rtcwake"));
1321     parent->window()->winId();
1322     action.setParentWindow(parent->window()->windowHandle());
1323     action.setArguments(args);
1324     KAuth::ExecuteJob* job = action.execute();
1325     if (!job->exec())
1326     {
1327         QString errmsg = job->errorString();
1328         qCDebug(KALARM_LOG) << "KAlarm::setRtcWakeTime: Error code=" << job->error() << errmsg;
1329         if (errmsg.isEmpty())
1330         {
1331             int errcode = job->error();
1332             switch (errcode)
1333             {
1334                 case KAuth::ActionReply::AuthorizationDeniedError:
1335                 case KAuth::ActionReply::UserCancelledError:
1336                     qCDebug(KALARM_LOG) << "KAlarm::setRtcWakeTime: Authorization error:" << errcode;
1337                     return false;   // the user should already know about this
1338                 default:
1339                     break;
1340             }
1341             errmsg = i18nc("@info", "Error obtaining authorization (%1)", errcode);
1342         }
1343         KAMessageBox::information(parent, errmsg);
1344         return false;
1345     }
1346     return true;
1347 }
1348 #endif // ENABLE_RTC_WAKE_FROM_SUSPEND
1349 
1350 } // namespace KAlarm
1351 namespace
1352 {
1353 
1354 /******************************************************************************
1355 * Update an event in its resource.
1356 */
1357 KAlarm::UpdateResult updateEvent(KAEvent& event, KAlarm::UpdateError err, QWidget* msgParent, bool saveIfReadOnly)
1358 {
1359     UpdateStatusData status;
1360     const KAEvent newEvent = ResourcesCalendar::updateEvent(event, saveIfReadOnly);
1361     if (!newEvent.isValid())
1362         status.status = KAlarm::UPDATE_FAILED;
1363     else
1364     {
1365         Resource resource = Resources::resource(event.resourceId());
1366         if ((saveIfReadOnly || !resource.readOnly())
1367         &&  !resource.save(&status.status.message))
1368         {
1369             resource.reload(true);   // retrieve the pre-update version of the event
1370             status.status.status = KAlarm::SAVE_FAILED;
1371         }
1372     }
1373     if (status.status != KAlarm::UPDATE_OK)
1374     {
1375         if (msgParent)
1376             displayUpdateError(msgParent, err, status);
1377     }
1378     return status.status;
1379 }
1380 
1381 /******************************************************************************
1382 * Create a new template.
1383 * 'preset' is non-null to base it on an existing event or template; otherwise,
1384 * the alarm type is set to 'type'.
1385 */
1386 void editNewTemplate(EditAlarmDlg::Type type, const KAEvent& preset, QWidget* parent)
1387 {
1388     if (Resources::enabledResources(CalEvent::TEMPLATE, true).isEmpty())
1389     {
1390         KAMessageBox::error(parent, i18nc("@info", "You must enable a template calendar to save the template in"));
1391         return;
1392     }
1393     // Use AutoQPointer to guard against crash on application exit while
1394     // the dialogue is still open. It prevents double deletion (both on
1395     // deletion of parent, and on return from this function).
1396     AutoQPointer<EditAlarmDlg> editDlg;
1397     if (preset.isValid())
1398         editDlg = EditAlarmDlg::create(true, preset, true, parent);
1399     else
1400         editDlg = EditAlarmDlg::create(true, type, parent);
1401     if (editDlg  &&  editDlg->exec() == QDialog::Accepted)
1402     {
1403         KAEvent event;
1404         Resource resource;
1405         editDlg->getEvent(event, resource);
1406 
1407         // Add the template to the displayed lists and to the calendar file
1408         KAlarm::addTemplate(event, resource, editDlg);
1409         Undo::saveAdd(event, resource);
1410     }
1411 }
1412 
1413 } // namespace
1414 namespace KAlarm
1415 {
1416 
1417 /******************************************************************************
1418 * Open the Edit Alarm dialog to edit the specified alarm.
1419 * If the alarm is read-only or archived, the dialog is opened read-only.
1420 */
1421 void editAlarm(KAEvent& event, QWidget* parent)
1422 {
1423     if (event.expired()  ||  eventReadOnly(event.id()))
1424     {
1425         viewAlarm(event, parent);
1426         return;
1427     }
1428     const EventId id(event);
1429     // Use AutoQPointer to guard against crash on application exit while
1430     // the dialogue is still open. It prevents double deletion (both on
1431     // deletion of parent, and on return from this function).
1432     AutoQPointer<EditAlarmDlg> editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID);
1433     if (editDlg  &&  editDlg->exec() == QDialog::Accepted)
1434     {
1435         if (!ResourcesCalendar::event(id).isValid())
1436         {
1437             // Event has been deleted while the user was editing the alarm,
1438             // so treat it as a new alarm.
1439             PrivateNewAlarmDlg().accept(editDlg);
1440             return;
1441         }
1442         KAEvent newEvent;
1443         Resource resource;
1444         bool changeDeferral = !editDlg->getEvent(newEvent, resource);
1445 
1446         // Update the event in the displays and in the calendar file
1447         const Undo::Event undo(event, resource);
1448         if (changeDeferral)
1449         {
1450             // The only change has been to an existing deferral
1451             if (updateEvent(newEvent, editDlg, true, true) != UPDATE_OK)   // keep the same event ID
1452                 return;   // failed to save event
1453         }
1454         else
1455         {
1456             const UpdateResult status = modifyEvent(event, newEvent, editDlg);
1457             if (status.status != UPDATE_OK  &&  status.status <= UPDATE_KORG_ERR)
1458                 displayKOrgUpdateError(editDlg, ERR_MODIFY, status);
1459         }
1460         Undo::saveEdit(undo, newEvent);
1461 
1462         outputAlarmWarnings(editDlg, &newEvent);
1463     }
1464 }
1465 
1466 /******************************************************************************
1467 * Display the alarm edit dialog to edit the alarm with the specified ID.
1468 * An error occurs if the alarm is not found, if there is more than one alarm
1469 * with the same ID, or if it is read-only or expired.
1470 */
1471 bool editAlarmById(const EventId& id, QWidget* parent)
1472 {
1473     const QString eventID(id.eventId());
1474     KAEvent event = ResourcesCalendar::event(id, true);
1475     if (!event.isValid())
1476     {
1477         if (id.resourceId() != -1)
1478             qCWarning(KALARM_LOG) << "KAlarm::editAlarmById: Event ID not found, or duplicated:" << eventID;
1479         else
1480             qCWarning(KALARM_LOG) << "KAlarm::editAlarmById: Event ID not found:" << eventID;
1481         return false;
1482     }
1483     if (eventReadOnly(event.id()))
1484     {
1485         qCCritical(KALARM_LOG) << "KAlarm::editAlarmById:" << eventID << ": read-only";
1486         return false;
1487     }
1488     switch (event.category())
1489     {
1490         case CalEvent::ACTIVE:
1491         case CalEvent::TEMPLATE:
1492             break;
1493         default:
1494             qCCritical(KALARM_LOG) << "KAlarm::editAlarmById:" << eventID << ": event not active or template";
1495             return false;
1496     }
1497     editAlarm(event, parent);
1498     return true;
1499 }
1500 
1501 /******************************************************************************
1502 * Open the Edit Alarm dialog to edit the specified template.
1503 * If the template is read-only, the dialog is opened read-only.
1504 */
1505 void editTemplate(const KAEvent& event, QWidget* parent)
1506 {
1507     if (eventReadOnly(event.id()))
1508     {
1509         // The template is read-only, so make the dialogue read-only.
1510         // Use AutoQPointer to guard against crash on application exit while
1511         // the dialogue is still open. It prevents double deletion (both on
1512         // deletion of parent, and on return from this function).
1513         AutoQPointer<EditAlarmDlg> editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_PROMPT, true);
1514         if (editDlg)
1515             editDlg->exec();
1516         return;
1517     }
1518     // Use AutoQPointer to guard against crash on application exit while
1519     // the dialogue is still open. It prevents double deletion (both on
1520     // deletion of parent, and on return from this function).
1521     AutoQPointer<EditAlarmDlg> editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID);
1522     if (editDlg  &&  editDlg->exec() == QDialog::Accepted)
1523     {
1524         KAEvent newEvent;
1525         Resource resource;
1526         editDlg->getEvent(newEvent, resource);
1527         const QString id = event.id();
1528         newEvent.setEventId(id);
1529         newEvent.setResourceId(event.resourceId());
1530 
1531         // Update the event in the displays and in the calendar file
1532         const Undo::Event undo(event, resource);
1533         updateTemplate(newEvent, editDlg);
1534         Undo::saveEdit(undo, newEvent);
1535     }
1536 }
1537 
1538 /******************************************************************************
1539 * Open the Edit Alarm dialog to view the specified alarm (read-only).
1540 */
1541 void viewAlarm(const KAEvent& event, QWidget* parent)
1542 {
1543     // Use AutoQPointer to guard against crash on application exit while
1544     // the dialogue is still open. It prevents double deletion (both on
1545     // deletion of parent, and on return from this function).
1546     AutoQPointer<EditAlarmDlg> editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_PROMPT, true);
1547     if (editDlg)
1548         editDlg->exec();
1549 }
1550 
1551 /******************************************************************************
1552 * Called when OK is clicked in the alarm edit dialog invoked by the Edit button
1553 * in an alarm message window.
1554 * Updates the alarm calendar and closes the dialog.
1555 */
1556 void updateEditedAlarm(EditAlarmDlg* editDlg, KAEvent& event, Resource& resource)
1557 {
1558     qCDebug(KALARM_LOG) << "KAlarm::updateEditedAlarm";
1559     KAEvent newEvent;
1560     Resource res;
1561     editDlg->getEvent(newEvent, res);
1562 
1563     // Update the displayed lists and the calendar file
1564     UpdateResult status;
1565     if (ResourcesCalendar::event(EventId(event)).isValid())
1566     {
1567         // The old alarm hasn't expired yet, so replace it
1568         const Undo::Event undo(event, resource);
1569         status = modifyEvent(event, newEvent, editDlg);
1570         Undo::saveEdit(undo, newEvent);
1571     }
1572     else
1573     {
1574         // The old event has expired, so simply create a new one
1575         status = addEvent(newEvent, resource, editDlg);
1576         Undo::saveAdd(newEvent, resource);
1577     }
1578 
1579     if (status.status != UPDATE_OK  &&  status.status <= UPDATE_KORG_ERR)
1580         displayKOrgUpdateError(editDlg, ERR_MODIFY, status);
1581     outputAlarmWarnings(editDlg, &newEvent);
1582 
1583     editDlg->close();
1584 }
1585 
1586 /******************************************************************************
1587 * Returns a list of all alarm templates.
1588 * If shell commands are disabled, command alarm templates are omitted.
1589 */
1590 QList<KAEvent> templateList()
1591 {
1592     QList<KAEvent> templates;
1593     const bool includeCmdAlarms = ShellProcess::authorised();
1594     const QList<KAEvent> events = ResourcesCalendar::events(CalEvent::TEMPLATE);
1595     for (const KAEvent& event : events)
1596     {
1597         if (includeCmdAlarms  ||  !(event.actionTypes() & KAEvent::Action::Command))
1598             templates.append(event);
1599     }
1600     return templates;
1601 }
1602 
1603 /******************************************************************************
1604 * To be called after an alarm has been edited.
1605 * Prompt the user to re-enable alarms if they are currently disabled, and if
1606 * it's an email alarm, warn if no 'From' email address is configured.
1607 */
1608 void outputAlarmWarnings(QWidget* parent, const KAEvent* event)
1609 {
1610     if (event  &&  event->actionTypes() == KAEvent::Action::Email
1611     &&  Preferences::emailAddress().isEmpty())
1612         KAMessageBox::information(parent, xi18nc("@info Please set the 'From' email address...",
1613                                                 "<para>%1</para><para>Please set it in the Configuration dialog.</para>", KAMail::i18n_NeedFromEmailAddress()));
1614 
1615     if (!theApp()->alarmsEnabled())
1616     {
1617         if (KAMessageBox::warningYesNo(parent, xi18nc("@info", "<para>Alarms are currently disabled.</para><para>Do you want to enable alarms now?</para>"),
1618                                        QString(), KGuiItem(i18nc("@action:button", "Enable")), KGuiItem(i18nc("@action:button", "Keep Disabled")),
1619                                        QStringLiteral("EditEnableAlarms"))
1620                         == KMessageBox::ButtonCode::PrimaryAction)
1621             theApp()->setAlarmsEnabled(true);
1622     }
1623 }
1624 
1625 /******************************************************************************
1626 * Reload the calendar.
1627 */
1628 void refreshAlarms()
1629 {
1630     qCDebug(KALARM_LOG) << "KAlarm::refreshAlarms";
1631     if (!refreshAlarmsQueued)
1632     {
1633         refreshAlarmsQueued = true;
1634         theApp()->processQueue();
1635     }
1636 }
1637 
1638 /******************************************************************************
1639 * This method must only be called from the main KAlarm queue processing loop,
1640 * to prevent asynchronous calendar operations interfering with one another.
1641 *
1642 * If refreshAlarms() has been called, reload the calendars.
1643 */
1644 void refreshAlarmsIfQueued()
1645 {
1646     if (refreshAlarmsQueued)
1647     {
1648         qCDebug(KALARM_LOG) << "KAlarm::refreshAlarmsIfQueued";
1649         QList<Resource> resources = Resources::enabledResources();
1650         for (Resource& resource : resources)
1651             resource.reload();
1652 
1653         // Close any message displays for alarms which are now disabled
1654         const QList<KAEvent> events = ResourcesCalendar::events(CalEvent::ACTIVE);
1655         for (const KAEvent& event : events)
1656         {
1657             if (!event.enabled()  &&  (event.actionTypes() & KAEvent::Action::Display))
1658             {
1659                 MessageDisplay* win = MessageDisplay::findEvent(EventId(event));
1660                 delete win;
1661             }
1662         }
1663 
1664         MainWindow::refresh();
1665         refreshAlarmsQueued = false;
1666     }
1667 }
1668 
1669 /******************************************************************************
1670 * The "Don't show again" option for error messages is personal to the user on a
1671 * particular computer. For example, he may want to inhibit error messages only
1672 * on his laptop. So the status is not stored in the alarm calendar, but in the
1673 * user's local KAlarm data directory.
1674 ******************************************************************************/
1675 
1676 /******************************************************************************
1677 * Return the Don't-show-again error message tags set for a specified alarm ID.
1678 */
1679 QStringList dontShowErrors(const EventId& eventId)
1680 {
1681     if (eventId.isEmpty())
1682         return {};
1683     KConfig config(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE);
1684     KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP);
1685     const QString id = QStringLiteral("%1:%2").arg(eventId.resourceId()).arg(eventId.eventId());
1686     return group.readEntry(id, QStringList());
1687 }
1688 
1689 /******************************************************************************
1690 * Check whether the specified Don't-show-again error message tag is set for an
1691 * alarm ID.
1692 */
1693 bool dontShowErrors(const EventId& eventId, const QString& tag)
1694 {
1695     if (tag.isEmpty())
1696         return false;
1697     const QStringList tags = dontShowErrors(eventId);
1698     return tags.indexOf(tag) >= 0;
1699 }
1700 
1701 /******************************************************************************
1702 * Reset the Don't-show-again error message tags for an alarm ID.
1703 * If 'tags' is empty, the config entry is deleted.
1704 */
1705 void setDontShowErrors(const EventId& eventId, const QStringList& tags)
1706 {
1707     if (eventId.isEmpty())
1708         return;
1709     KConfig config(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE);
1710     KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP);
1711     const QString id = QStringLiteral("%1:%2").arg(eventId.resourceId()).arg(eventId.eventId());
1712     if (tags.isEmpty())
1713         group.deleteEntry(id);
1714     else
1715         group.writeEntry(id, tags);
1716     group.sync();
1717 }
1718 
1719 /******************************************************************************
1720 * Set the specified Don't-show-again error message tag for an alarm ID.
1721 * Existing tags are unaffected.
1722 */
1723 void setDontShowErrors(const EventId& eventId, const QString& tag)
1724 {
1725     if (eventId.isEmpty()  ||  tag.isEmpty())
1726         return;
1727     KConfig config(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE);
1728     KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP);
1729     const QString id = QStringLiteral("%1:%2").arg(eventId.resourceId()).arg(eventId.eventId());
1730     QStringList tags = group.readEntry(id, QStringList());
1731     if (tags.indexOf(tag) < 0)
1732     {
1733         tags += tag;
1734         group.writeEntry(id, tags);
1735         group.sync();
1736     }
1737 }
1738 
1739 /******************************************************************************
1740 * Convert a date/time specification string into a local date/time or date value.
1741 * Parameters:
1742 *   timeString  = in the form [[[yyyy-]mm-]dd-]hh:mm [TZ] or yyyy-mm-dd [TZ].
1743 *   dateTime  = receives converted date/time value.
1744 *   defaultDt = default date/time used for missing parts of timeString, or null
1745 *               to use current date/time.
1746 *   allowTZ   = whether to allow a time zone specifier in timeString.
1747 * Reply = true if successful.
1748 */
1749 bool convertTimeString(const QByteArray& timeString, KADateTime& dateTime, const KADateTime& defaultDt, bool allowTZ)
1750 {
1751 #define MAX_DT_LEN 19
1752     int i = timeString.indexOf(' ');
1753     if (i > MAX_DT_LEN  ||  (i >= 0 && !allowTZ))
1754         return false;
1755     QString zone = (i >= 0) ? QString::fromLatin1(timeString.mid(i)) : QString();
1756     char timeStr[MAX_DT_LEN+1];
1757     strncpy(timeStr, timeString.left(i >= 0 ? i : MAX_DT_LEN).constData(), MAX_DT_LEN);
1758     timeStr[MAX_DT_LEN] = '\0';
1759     int dt[5] = { -1, -1, -1, -1, -1 };
1760     char* s;
1761     char* end;
1762     bool noTime;
1763     // Get the minute value
1764     if ((s = strchr(timeStr, ':')) == nullptr)
1765         noTime = true;
1766     else
1767     {
1768         noTime = false;
1769         *s++ = 0;
1770         dt[4] = strtoul(s, &end, 10);
1771         if (end == s  ||  *end  ||  dt[4] >= 60)
1772             return false;
1773         // Get the hour value
1774         if ((s = strrchr(timeStr, '-')) == nullptr)
1775             s = timeStr;
1776         else
1777             *s++ = 0;
1778         dt[3] = strtoul(s, &end, 10);
1779         if (end == s  ||  *end  ||  dt[3] >= 24)
1780             return false;
1781     }
1782     bool noDate = true;
1783     if (s != timeStr)
1784     {
1785         noDate = false;
1786         // Get the day value
1787         if ((s = strrchr(timeStr, '-')) == nullptr)
1788             s = timeStr;
1789         else
1790             *s++ = 0;
1791         dt[2] = strtoul(s, &end, 10);
1792         if (end == s  ||  *end  ||  dt[2] == 0  ||  dt[2] > 31)
1793             return false;
1794         if (s != timeStr)
1795         {
1796             // Get the month value
1797             if ((s = strrchr(timeStr, '-')) == nullptr)
1798                 s = timeStr;
1799             else
1800                 *s++ = 0;
1801             dt[1] = strtoul(s, &end, 10);
1802             if (end == s  ||  *end  ||  dt[1] == 0  ||  dt[1] > 12)
1803                 return false;
1804             if (s != timeStr)
1805             {
1806                 // Get the year value
1807                 dt[0] = strtoul(timeStr, &end, 10);
1808                 if (end == timeStr  ||  *end)
1809                     return false;
1810             }
1811         }
1812     }
1813 
1814     QDate date;
1815     if (dt[0] >= 0)
1816         date = QDate(dt[0], dt[1], dt[2]);
1817     QTime time(0, 0, 0);
1818     if (noTime)
1819     {
1820         // No time was specified, so the full date must have been specified
1821         if (dt[0] < 0  ||  !date.isValid())
1822             return false;
1823         dateTime = applyTimeZone(zone, date, time, false, defaultDt);
1824     }
1825     else
1826     {
1827         // Compile the values into a date/time structure
1828         time.setHMS(dt[3], dt[4], 0);
1829         if (dt[0] < 0)
1830         {
1831             // Some or all of the date was omitted.
1832             // Use the default date/time if provided.
1833             if (defaultDt.isValid())
1834             {
1835                 dt[0] = defaultDt.date().year();
1836                 date.setDate(dt[0],
1837                             (dt[1] < 0 ? defaultDt.date().month() : dt[1]),
1838                             (dt[2] < 0 ? defaultDt.date().day() : dt[2]));
1839             }
1840             else
1841                 date.setDate(2000, 1, 1);  // temporary substitute for date
1842         }
1843         dateTime = applyTimeZone(zone, date, time, true, defaultDt);
1844         if (!dateTime.isValid())
1845             return false;
1846         if (dt[0] < 0)
1847         {
1848             // Some or all of the date was omitted.
1849             // Use the current date in the specified time zone as default.
1850             const KADateTime now = KADateTime::currentDateTime(dateTime.timeSpec());
1851             date = dateTime.date();
1852             date.setDate(now.date().year(),
1853                         (dt[1] < 0 ? now.date().month() : dt[1]),
1854                         (dt[2] < 0 ? now.date().day() : dt[2]));
1855             if (!date.isValid())
1856                 return false;
1857             if (noDate  &&  time < now.time())
1858                 date = date.addDays(1);
1859             dateTime.setDate(date);
1860         }
1861     }
1862     return dateTime.isValid();
1863 }
1864 
1865 /******************************************************************************
1866 * Extract dragged and dropped Akonadi RFC822 message data.
1867 */
1868 bool dropAkonadiEmail(const QMimeData* data, QUrl& url, AlarmText& alarmText)
1869 {
1870     alarmText.clear();
1871     const QList<QUrl> urls = data->urls();
1872     if (urls.isEmpty())
1873         url = QUrl();
1874     else
1875     {
1876         url  = urls.at(0);
1877         AkonadiPlugin* akonadiPlugin = Preferences::akonadiPlugin();
1878         if (akonadiPlugin)
1879         {
1880             KAEvent::EmailId emailId;
1881             KMime::Message::Ptr message = akonadiPlugin->fetchAkonadiEmail(url, emailId);
1882             if (message)
1883             {
1884                 // It's an email held in Akonadi
1885                 if (message->hasContent())
1886                     alarmText = DragDrop::kMimeEmailToAlarmText(*message, emailId);
1887                 return true;
1888             }
1889         }
1890     }
1891     return false;
1892 }
1893 
1894 /******************************************************************************
1895 * Convert a time zone specifier string and apply it to a given date and/or time.
1896 * The time zone specifier is a system time zone name, e.g. "Europe/London" or
1897 * "UTC". If no time zone is specified, it defaults to the local time zone.
1898 * If 'defaultDt' is valid, it supplies the time spec and default date.
1899 */
1900 KADateTime applyTimeZone(const QString& tzstring, const QDate& date, const QTime& time,
1901                          bool haveTime, const KADateTime& defaultDt)
1902 {
1903     bool error = false;
1904     KADateTime::Spec spec = KADateTime::LocalZone;
1905     const QString zone = tzstring.trimmed();
1906     if (!zone.isEmpty())
1907     {
1908         if (zone == QLatin1String("UTC"))
1909             spec = KADateTime::UTC;
1910         else
1911         {
1912             const QTimeZone tz(zone.toLatin1());
1913             error = !tz.isValid();
1914             if (!error)
1915                 spec = tz;
1916         }
1917     }
1918     else if (defaultDt.isValid())
1919         spec = defaultDt.timeSpec();
1920 
1921     KADateTime result;
1922     if (!error)
1923     {
1924         if (!date.isValid())
1925         {
1926             // It's a time without a date
1927             if (defaultDt.isValid())
1928                    result = KADateTime(defaultDt.date(), time, spec);
1929             else if (spec == KADateTime::LocalZone)
1930                 result = KADateTime(KADateTime::currentLocalDate(), time, spec);
1931         }
1932         else if (haveTime)
1933         {
1934             // It's a date and time
1935             result = KADateTime(date, time, spec);
1936         }
1937         else
1938         {
1939             // It's a date without a time
1940             result = KADateTime(date, spec);
1941         }
1942     }
1943     return result;
1944 }
1945 
1946 #ifndef NDEBUG
1947 /******************************************************************************
1948 * Set up KAlarm test conditions based on environment variables.
1949 * KALARM_TIME: specifies current system time (format [[[yyyy-]mm-]dd-]hh:mm [TZ]).
1950 */
1951 void setTestModeConditions()
1952 {
1953     const QByteArray newTime = qgetenv("KALARM_TIME");
1954     if (!newTime.isEmpty())
1955     {
1956         KADateTime dt;
1957         if (convertTimeString(newTime, dt, KADateTime::realCurrentLocalDateTime(), true))
1958             setSimulatedSystemTime(dt);
1959     }
1960 }
1961 
1962 /******************************************************************************
1963 * Set the simulated system time.
1964 */
1965 void setSimulatedSystemTime(const KADateTime& dt)
1966 {
1967     KADateTime::setSimulatedSystemTime(dt);
1968     qCDebug(KALARM_LOG) << "New time =" << qPrintable(KADateTime::currentLocalDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M %:Z")));
1969 }
1970 #endif
1971 
1972 } // namespace KAlarm
1973 namespace
1974 {
1975 
1976 /******************************************************************************
1977 * Display an error message about an error when saving an event.
1978 */
1979 void displayUpdateError(QWidget* parent, KAlarm::UpdateError code, const UpdateStatusData& status, bool showKOrgError)
1980 {
1981     QString errmsg;
1982     if (status.status.status > KAlarm::UPDATE_KORG_ERR)
1983     {
1984         switch (code)
1985         {
1986             case KAlarm::ERR_ADD:
1987             case KAlarm::ERR_MODIFY:
1988                 errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarms")
1989                                               : i18nc("@info", "Error saving alarm");
1990                 break;
1991             case KAlarm::ERR_DELETE:
1992                 errmsg = (status.warnErr > 1) ? i18nc("@info", "Error deleting alarms")
1993                                               : i18nc("@info", "Error deleting alarm");
1994                 break;
1995             case KAlarm::ERR_REACTIVATE:
1996                 errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving reactivated alarms")
1997                                               : i18nc("@info", "Error saving reactivated alarm");
1998                 break;
1999             case KAlarm::ERR_TEMPLATE:
2000                 errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarm templates")
2001                                               : i18nc("@info", "Error saving alarm template");
2002                 break;
2003         }
2004         if (!status.status.message.isEmpty())
2005             KAMessageBox::detailedError(parent, errmsg, status.status.message);
2006         else
2007             KAMessageBox::error(parent, errmsg);
2008     }
2009     else if (showKOrgError)
2010         displayKOrgUpdateError(parent, code, status.status, status.warnKOrg);
2011 }
2012 
2013 /******************************************************************************
2014 * Tell KOrganizer to put an alarm in its calendar.
2015 * It will be held by KOrganizer as a simple event, without alarms - KAlarm
2016 * is still responsible for alarming.
2017 */
2018 KAlarm::UpdateResult sendToKOrganizer(const KAEvent& event)
2019 {
2020     Event::Ptr kcalEvent(new KCalendarCore::Event);
2021     event.updateKCalEvent(kcalEvent, KAEvent::UidAction::Ignore);
2022     // Change the event ID to avoid duplicating the same unique ID as the original event
2023     const QString uid = uidKOrganizer(event.id());
2024     kcalEvent->setUid(uid);
2025     kcalEvent->clearAlarms();
2026     QString userEmail;
2027     switch (event.actionTypes())
2028     {
2029         case KAEvent::Action::Display:
2030         case KAEvent::Action::Command:
2031         case KAEvent::Action::DisplayCommand:
2032             kcalEvent->setSummary(event.cleanText());
2033             userEmail = Preferences::emailAddress();
2034             break;
2035         case KAEvent::Action::Email:
2036         {
2037             const QString from = event.emailFromId()
2038                                ? Identities::identityManager()->identityForUoid(event.emailFromId()).fullEmailAddr()
2039                                : Preferences::emailAddress();
2040             AlarmText atext;
2041             atext.setEmail(event.emailAddresses(QStringLiteral(", ")), from, QString(), QString(), event.emailSubject(), QString());
2042             kcalEvent->setSummary(atext.displayText());
2043             userEmail = from;
2044             break;
2045         }
2046         case KAEvent::Action::Audio:
2047             kcalEvent->setSummary(event.audioFile());
2048             break;
2049         default:
2050             break;
2051     }
2052     const Person person(QString(), userEmail);
2053     kcalEvent->setOrganizer(person);
2054     kcalEvent->setDuration(Duration(Preferences::kOrgEventDuration() * 60, Duration::Seconds));
2055 
2056     // Translate the event into string format
2057     ICalFormat format;
2058     format.setTimeZone(Preferences::timeSpecAsZone());
2059     const QString iCal = format.toICalString(kcalEvent);
2060 
2061     // Send the event to KOrganizer
2062     KAlarm::UpdateResult status = runKOrganizer();   // start KOrganizer if it isn't already running, and create its D-Bus interface
2063     if (status != KAlarm::UPDATE_OK)
2064         return status;
2065     QDBusInterface korgInterface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_PATH), KORG_DBUS_IFACE);
2066     const QList<QVariant> args{iCal};
2067     QDBusReply<bool> reply = korgInterface.callWithArgumentList(QDBus::Block, QStringLiteral("addIncidence"), args);
2068     if (!reply.isValid())
2069     {
2070         if (reply.error().type() == QDBusError::UnknownObject)
2071         {
2072             status =  KAlarm::UPDATE_KORG_ERRSTART;
2073             qCCritical(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence() D-Bus error: still starting";
2074         }
2075         else
2076         {
2077             status.set(KAlarm::UPDATE_KORG_ERR, reply.error().message());
2078             qCCritical(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence(" << uid << ") D-Bus call failed:" << status.message;
2079         }
2080     }
2081     else if (!reply.value())
2082     {
2083         status = KAlarm::UPDATE_KORG_FUNCERR;
2084         qCDebug(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence(" << uid << ") D-Bus call returned false";
2085     }
2086     else
2087         qCDebug(KALARM_LOG) << "KAlarm::sendToKOrganizer:" << uid << ": success";
2088     return status;
2089 }
2090 
2091 /******************************************************************************
2092 * Tell KOrganizer to delete an event from its calendar.
2093 */
2094 KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID)
2095 {
2096     AkonadiPlugin* akonadiPlugin = Preferences::akonadiPlugin();
2097     if (!akonadiPlugin)
2098         return KAlarm::UpdateResult(KAlarm::UPDATE_KORG_ERR);
2099 
2100     const QString newID = uidKOrganizer(eventID);
2101     akonadiPlugin->deleteEvent(KORG_MIME_TYPE, QString(), newID);
2102     // Ignore errors
2103     return KAlarm::UpdateResult(KAlarm::UPDATE_OK);
2104 }
2105 
2106 /******************************************************************************
2107 * Start KOrganizer if not already running, and create its D-Bus interface.
2108 */
2109 KAlarm::UpdateResult runKOrganizer()
2110 {
2111     KAlarm::UpdateResult status;
2112 
2113     // If Kontact is running, there is a load() method which needs to be called to
2114     // load KOrganizer into Kontact. But if KOrganizer is running independently,
2115     // the load() method doesn't exist. This call starts korganizer if needed, too.
2116     QDBusInterface iface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_LOAD_PATH), QStringLiteral("org.kde.PIMUniqueApplication"));
2117     QDBusReply<bool> reply = iface.call(QStringLiteral("load"));
2118     if ((!reply.isValid() || !reply.value())
2119     &&  iface.lastError().type() != QDBusError::UnknownMethod)
2120     {
2121         status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message());
2122         qCWarning(KALARM_LOG) << "Loading KOrganizer failed:" << status.message;
2123         return status;
2124     }
2125 
2126     return status;
2127 }
2128 
2129 /******************************************************************************
2130 * Insert a KOrganizer string after the hyphen in the supplied event ID.
2131 */
2132 QString uidKOrganizer(const QString& id)
2133 {
2134     if (id.startsWith(KORGANIZER_UID))
2135         return id;
2136     QString result = id;
2137     return result.insert(0, KORGANIZER_UID);
2138 }
2139 
2140 } // namespace
2141 
2142 /******************************************************************************
2143 * Case insensitive comparison for use by qSort().
2144 */
2145 bool caseInsensitiveLessThan(const QString& s1, const QString& s2)
2146 {
2147     return s1.toLower() < s2.toLower();
2148 }
2149 
2150 #include "moc_functions_p.cpp"
2151 
2152 // vim: et sw=4: