1 /*
2  *  kalarmapp.cpp  -  the KAlarm application object
3  *  Program:  kalarm
4  *  SPDX-FileCopyrightText: 2001-2021 David Jarvie <djarvie@kde.org>
5  *
6  *  SPDX-License-Identifier: GPL-2.0-or-later
7  */
8 
9 #include "kalarmapp.h"
10 
11 #include "kalarm.h"
12 #include "commandoptions.h"
13 #include "dbushandler.h"
14 #include "displaycalendar.h"
15 #include "editdlgtypes.h"
16 #include "functions.h"
17 #include "mainwindow.h"
18 #include "messagewindow.h"
19 #include "messagenotification.h"
20 #include "prefdlg.h"
21 #include "resourcescalendar.h"
22 #include "startdaytimer.h"
23 #include "traywindow.h"
24 #include "resources/datamodel.h"
25 #include "resources/resources.h"
26 #include "resources/eventmodel.h"
27 #include "lib/desktop.h"
28 #include "lib/messagebox.h"
29 #include "lib/shellprocess.h"
30 #include "notifications_interface.h" // DBUS-generated
31 #include "dbusproperties.h"          // DBUS-generated
32 #include "kalarm_debug.h"
33 #include <kcoreaddons_version.h>
34 #if KCOREADDONS_VERSION < QT_VERSION_CHECK(6, 0, 0)
35 #include "migratekde4files.h"
36 #endif
37 #include <KAlarmCal/DateTime>
38 #include <KAlarmCal/KARecurrence>
39 
40 #include <KLocalizedString>
41 #include <KConfig>
42 #include <KConfigGui>
43 #include <KAboutData>
44 #include <KSharedConfig>
45 #include <KStandardGuiItem>
46 #include <netwm.h>
47 #include <KShell>
48 
49 #include <QObject>
50 #include <QTimer>
51 #include <QFile>
52 #include <QTextStream>
53 #include <QTemporaryFile>
54 #include <QStandardPaths>
55 #include <QSystemTrayIcon>
56 #include <QCommandLineParser>
57 
58 #include <stdlib.h>
59 #include <ctype.h>
60 #include <iostream>
61 #include <climits>
62 
63 namespace
64 {
65 const int RESOURCES_TIMEOUT = 30;   // timeout (seconds) for resources to be populated
66 
67 const char FDO_NOTIFICATIONS_SERVICE[] = "org.freedesktop.Notifications";
68 const char FDO_NOTIFICATIONS_PATH[]    = "/org/freedesktop/Notifications";
69 
70 /******************************************************************************
71 * Find the maximum number of seconds late which a late-cancel alarm is allowed
72 * to be. This is calculated as the late cancel interval, plus a few seconds
73 * leeway to cater for any timing irregularities.
74 */
maxLateness(int lateCancel)75 inline int maxLateness(int lateCancel)
76 {
77     static const int LATENESS_LEEWAY = 5;
78     int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0;
79     return LATENESS_LEEWAY + lc;
80 }
81 
mainWidget()82 QWidget* mainWidget()
83 {
84     return MainWindow::mainMainWindow();
85 }
86 }
87 
88 
89 KAlarmApp*  KAlarmApp::mInstance  = nullptr;
90 int         KAlarmApp::mActiveCount = 0;
91 int         KAlarmApp::mFatalError  = 0;
92 QString     KAlarmApp::mFatalMessage;
93 
94 
95 /******************************************************************************
96 * Construct the application.
97 */
KAlarmApp(int & argc,char ** argv)98 KAlarmApp::KAlarmApp(int& argc, char** argv)
99     : QApplication(argc, argv)
100     , mDBusHandler(new DBusHandler())
101 {
102     qCDebug(KALARM_LOG) << "KAlarmApp:";
103 }
104 
105 /******************************************************************************
106 */
~KAlarmApp()107 KAlarmApp::~KAlarmApp()
108 {
109     while (!mCommandProcesses.isEmpty())
110     {
111         ProcData* pd = mCommandProcesses.at(0);
112         mCommandProcesses.pop_front();
113         delete pd;
114     }
115     ResourcesCalendar::terminate();
116     DisplayCalendar::terminate();
117     DataModel::terminate();
118     delete mDBusHandler;
119 }
120 
121 /******************************************************************************
122 * Return the one and only KAlarmApp instance.
123 * If it doesn't already exist, it is created first.
124 */
create(int & argc,char ** argv)125 KAlarmApp* KAlarmApp::create(int& argc, char** argv)
126 {
127     if (!mInstance)
128     {
129         mInstance = new KAlarmApp(argc, argv);
130 
131         if (mFatalError)
132             mInstance->quitFatal();
133     }
134     return mInstance;
135 }
136 
137 /******************************************************************************
138 * Perform initialisations which may require KAboutData to have been set up.
139 */
initialise()140 void KAlarmApp::initialise()
141 {
142 #if KCOREADDONS_VERSION < QT_VERSION_CHECK(6, 0, 0)
143     // Migrate config and data files from KDE4 locations.
144     MigrateKde4Files migrate;
145     migrate.migrate();
146 #endif
147 
148 #ifndef NDEBUG
149     KAlarm::setTestModeConditions();
150 #endif
151 
152     setQuitOnLastWindowClosed(false);
153     Preferences::self();    // read KAlarm configuration
154     if (!Preferences::noAutoStart())
155     {
156         // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop
157         Preferences::setNoAutoStart(false);
158         // Enable kalarm.autostart.desktop to start KAlarm
159         Preferences::setAutoStart(true);
160         Preferences::self()->save();
161     }
162     Preferences::connect(&Preferences::startOfDayChanged, this, &KAlarmApp::changeStartOfDay);
163     Preferences::connect(&Preferences::workTimeChanged, this, &KAlarmApp::slotWorkTimeChanged);
164     Preferences::connect(&Preferences::holidaysChanged, this, &KAlarmApp::slotHolidaysChanged);
165     Preferences::connect(&Preferences::feb29TypeChanged, this, &KAlarmApp::slotFeb29TypeChanged);
166     Preferences::connect(&Preferences::showInSystemTrayChanged, this, &KAlarmApp::slotShowInSystemTrayChanged);
167     Preferences::connect(&Preferences::archivedKeepDaysChanged, this, &KAlarmApp::setArchivePurgeDays);
168     Preferences::connect(&Preferences::messageFontChanged, this, &KAlarmApp::slotMessageFontChanged);
169     slotFeb29TypeChanged(Preferences::defaultFeb29Type());
170 
171     KAEvent::setStartOfDay(Preferences::startOfDay());
172     KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd());
173     KAEvent::setHolidays(Preferences::holidays());
174     KAEvent::setDefaultFont(Preferences::messageFont());
175 
176     // Check if KOrganizer is installed
177     const QString korg = QStringLiteral("korganizer");
178     mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty();
179     if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; }
180     // Check if the window manager can't handle keyboard focus transfer between windows
181     mWindowFocusBroken = (Desktop::currentIdentity() == Desktop::Unity);
182     if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; }
183 
184     Resources* resources = Resources::instance();
185     connect(resources, &Resources::resourceAdded,
186                  this, &KAlarmApp::slotResourceAdded);
187     connect(resources, &Resources::resourcePopulated,
188                  this, &KAlarmApp::slotResourcePopulated);
189     connect(resources, &Resources::resourcePopulated,
190                  this, &KAlarmApp::purgeNewArchivedDefault);
191     connect(resources, &Resources::resourcesCreated,
192                  this, &KAlarmApp::slotResourcesCreated);
193     connect(resources, &Resources::resourcesPopulated,
194                  this, &KAlarmApp::processQueue);
195 
196     initialiseTimerResources();   // initialise calendars and alarm timer
197 
198     KConfigGroup config(KSharedConfig::openConfig(), "General");
199     mNoSystemTray        = config.readEntry("NoSystemTray", false);
200     mOldShowInSystemTray = wantShowInSystemTray();
201     DateTime::setStartOfDay(Preferences::startOfDay());
202     mPrefsArchivedColour = Preferences::archivedColour();
203 
204     // Get notified when the Freedesktop notifications properties have changed.
205     QDBusConnection conn = QDBusConnection::sessionBus();
206     if (conn.interface()->isServiceRegistered(QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE)))
207     {
208         OrgFreedesktopDBusPropertiesInterface* piface = new OrgFreedesktopDBusPropertiesInterface(
209                 QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE),
210                 QString::fromLatin1(FDO_NOTIFICATIONS_PATH),
211                 conn, this);
212         connect(piface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
213                 this, &KAlarmApp::slotFDOPropertiesChanged);
214         OrgFreedesktopNotificationsInterface niface(
215                 QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE),
216                 QString::fromLatin1(FDO_NOTIFICATIONS_PATH),
217                 conn);
218         mNotificationsInhibited = niface.inhibited();
219     }
220 }
221 
222 /******************************************************************************
223 * Initialise or reinitialise things which are tidied up/closed by quitIf().
224 * Reinitialisation can be necessary if session restoration finds nothing to
225 * restore and starts quitting the application, but KAlarm then starts up again
226 * before the application has exited.
227 * Reply = true if calendars were initialised successfully,
228 *         false if they were already initialised, or if initialisation failed.
229 */
initialiseTimerResources()230 bool KAlarmApp::initialiseTimerResources()
231 {
232     if (!mAlarmTimer)
233     {
234         mAlarmTimer = new QTimer(this);
235         mAlarmTimer->setSingleShot(true);
236         connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm);
237     }
238     if (!ResourcesCalendar::instance())
239     {
240         qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars";
241         Desktop::setMainWindowFunc(&mainWidget);
242         // First, initialise calendar resources, which need to be ready to
243         // receive signals when resources initialise.
244         ResourcesCalendar::initialise(KALARM_NAME, KALARM_VERSION);
245         connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm);
246         connect(ResourcesCalendar::instance(), &ResourcesCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded);
247         DisplayCalendar::initialise();
248         // Finally, initialise the resources which generate signals as they initialise.
249         DataModel::initialise();
250         return true;
251     }
252     return false;
253 }
254 
255 /******************************************************************************
256 * Restore the saved session if required.
257 */
restoreSession()258 bool KAlarmApp::restoreSession()
259 {
260     if (!isSessionRestored())
261         return false;
262     if (mFatalError)
263     {
264         quitFatal();
265         return false;
266     }
267 
268     // Process is being restored by session management.
269     qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring";
270     ++mActiveCount;
271     // Create the session config object now.
272     // This is necessary since if initCheck() below causes calendars to be updated,
273     // the session config created after that points to an invalid file, resulting
274     // in no windows being restored followed by a later crash.
275     KConfigGui::sessionConfig();
276 
277     // When KAlarm is session restored, automatically set start-at-login to true.
278     Preferences::self()->load();
279     Preferences::setAutoStart(true);
280     Preferences::setNoAutoStart(false);
281     Preferences::setAskAutoStart(true);  // cancel any start-at-login prompt suppression
282     Preferences::self()->save();
283 
284     if (!initCheck(true))     // open the calendar file (needed for main windows), don't process queue yet
285     {
286         --mActiveCount;
287         quitIf(1, true);    // error opening the main calendar - quit
288         return false;
289     }
290     MainWindow* trayParent = nullptr;
291     for (int i = 1;  KMainWindow::canBeRestored(i);  ++i)
292     {
293         const QString type = KMainWindow::classNameOfToplevel(i);
294         if (type == QLatin1String("MainWindow"))
295         {
296             MainWindow* win = MainWindow::create(true);
297             win->restore(i, false);
298             if (win->isHiddenTrayParent())
299                 trayParent = win;
300             else
301                 win->show();
302         }
303         else if (type == QLatin1String("MessageWindow"))
304         {
305             auto win = new MessageWindow;
306             win->restore(i, false);
307             if (win->isValid())
308             {
309                 if (Resources::allCreated()  &&  !mNotificationsInhibited)
310                     win->display();
311                 else
312                     mRestoredWindows += win;
313             }
314             else
315                 delete win;
316         }
317     }
318 
319     MessageNotification::sessionRestore();
320 
321     // Try to display the system tray icon if it is configured to be shown
322     if (trayParent  ||  wantShowInSystemTray())
323     {
324         if (!MainWindow::count())
325             qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to be restored!?";
326         else
327         {
328             displayTrayIcon(true, trayParent);
329             // Occasionally for no obvious reason, the main main window is
330             // shown when it should be hidden, so hide it just to be sure.
331             if (trayParent)
332                 trayParent->hide();
333         }
334     }
335 
336     --mActiveCount;
337     if (quitIf(0))          // quit if no windows are open
338         return false;       // quitIf() can sometimes return, despite calling exit()
339 
340     startProcessQueue();    // start processing the execution queue
341     return true;
342 }
343 
344 /******************************************************************************
345 * If resources have been created and notifications are not inhibited,
346 * show message windows restored at startup which are waiting to be displayed,
347 * and redisplay alarms showing when the program crashed or was killed.
348 */
showRestoredWindows()349 void KAlarmApp::showRestoredWindows()
350 {
351     if (!mNotificationsInhibited  &&  Resources::allCreated())
352     {
353         if (!mRestoredWindows.isEmpty())
354         {
355             // Display message windows restored at startup.
356             for (MessageWindow* win : std::as_const(mRestoredWindows))
357                 win->display();
358             mRestoredWindows.clear();
359         }
360         if (mRedisplayAlarms)
361         {
362             // Display alarms which were showing when the program crashed or was killed.
363             mRedisplayAlarms = false;
364             MessageDisplay::redisplayAlarms();
365         }
366     }
367 }
368 
369 /******************************************************************************
370 * Called to start a new instance of the unique QApplication.
371 * Reply: exit code (>= 0), or -1 to continue execution.
372 *        If exit code >= 0, 'outputText' holds text to output before terminating.
373 */
activateInstance(const QStringList & args,const QString & workingDirectory,QString * outputText)374 int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText)
375 {
376     Q_UNUSED(workingDirectory)
377     qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance" << args;
378     if (outputText)
379         outputText->clear();
380     if (mFatalError)
381     {
382         Q_EMIT setExitValue(1);
383         quitFatal();
384         return 1;
385     }
386 
387     // The D-Bus call to activate a subsequent instance of KAlarm may not supply
388     // any arguments, but we need one.
389     if (!args.isEmpty()  &&  mActivateArg0.isEmpty())
390         mActivateArg0 = args[0];
391     QStringList fixedArgs(args);
392     if (args.isEmpty()  &&  !mActivateArg0.isEmpty())
393         fixedArgs << mActivateArg0;
394 
395     // Parse and interpret command line arguments.
396     QCommandLineParser parser;
397     KAboutData::applicationData().setupCommandLine(&parser);
398     parser.setApplicationDescription(QApplication::applicationDisplayName());
399     auto options = new CommandOptions;
400     const QStringList nonexecArgs = options->setOptions(&parser, fixedArgs);
401     options->parse();
402     KAboutData::applicationData().processCommandLine(&parser);
403 
404     ++mActiveCount;
405     int exitCode = 0;               // default = success
406     static bool firstInstance = true;
407     bool dontRedisplay = false;
408     CommandOptions::Command command = CommandOptions::NONE;
409     const bool processOptions = (!firstInstance || !isSessionRestored());
410     if (processOptions)
411     {
412         options->process();
413 #ifndef NDEBUG
414         if (options->simulationTime().isValid())
415             KAlarm::setSimulatedSystemTime(options->simulationTime());
416 #endif
417         command = options->command();
418         if (options->disableAll())
419             setAlarmsEnabled(false);   // disable alarm monitoring
420 
421         // Handle options which exit with a terminal message, before
422         // making the application a unique application, since a
423         // unique application won't output to the terminal if another
424         // instance is already running.
425         switch (command)
426         {
427             case CommandOptions::CMD_ERROR:
428                 Q_EMIT setExitValue(1);
429                 if (outputText)
430                 {
431                     // Instance was activated from main().
432                     *outputText = options->outputText();
433                     delete options;
434                     return 1;
435                 }
436                 // Instance was activated by DBus.
437                 std::cerr << qPrintable(options->outputText()) << std::endl;;
438                 mReadOnly = true;   // don't need write access to calendars
439                 exitCode = 1;
440                 break;
441             default:
442                 break;
443         }
444     }
445 
446     if (processOptions)
447     {
448         switch (command)
449         {
450             case CommandOptions::TRIGGER_EVENT:
451             case CommandOptions::CANCEL_EVENT:
452             {
453                 // Display or delete the event with the specified event ID
454                 auto action = static_cast<QueuedAction>(int((command == CommandOptions::TRIGGER_EVENT) ? QueuedAction::Trigger : QueuedAction::Cancel)
455                                                       | int(QueuedAction::Exit));
456                 // Open the calendar, don't start processing execution queue yet,
457                 // and wait for the calendar resources to be populated.
458                 if (!initCheck(true))
459                     exitCode = 1;
460                 else
461                 {
462                     mCommandOption = options->commandName();
463                     // Get the resource ID string and event UID. Note that if
464                     // resources have not been created yet, the numeric
465                     // resource ID can't yet be looked up.
466                     if (options->resourceId().isEmpty())
467                         action = static_cast<QueuedAction>((int)action | int(QueuedAction::FindId));
468                     mActionQueue.enqueue(ActionQEntry(action, EventId(options->eventId()), options->resourceId()));
469                     startProcessQueue(true);      // start processing the execution queue
470                     dontRedisplay = true;
471                 }
472                 break;
473             }
474             case CommandOptions::LIST:
475                 // Output a list of scheduled alarms to stdout.
476                 // Open the calendar, don't start processing execution queue yet,
477                 // and wait for all calendar resources to be populated.
478                 mReadOnly = true;   // don't need write access to calendars
479                 if (firstInstance)
480                     mAlarmsEnabled = false;   // prevent alarms being processed if no other instance is running
481                 if (!initCheck(true))
482                     exitCode = 1;
483                 else
484                 {
485                     const auto action = static_cast<QueuedAction>(int(QueuedAction::List) | int(QueuedAction::Exit));
486                     mActionQueue.enqueue(ActionQEntry(action, EventId()));
487                     startProcessQueue(true);      // start processing the execution queue
488                     dontRedisplay = true;
489                 }
490                 break;
491 
492             case CommandOptions::EDIT:
493                 // Edit a specified existing alarm.
494                 // Open the calendar and wait for the calendar resources to be populated.
495                 if (!initCheck(false))
496                     exitCode = 1;
497                 else
498                 {
499                     mCommandOption = options->commandName();
500                     if (firstInstance)
501                         mEditingCmdLineAlarm = 0x10;   // want to redisplay alarms if successful
502                     // Get the resource ID string and event UID. Note that if
503                     // resources have not been created yet, the numeric
504                     // resource ID can't yet be looked up.
505                     mActionQueue.enqueue(ActionQEntry(QueuedAction::Edit, EventId(options->eventId()), options->resourceId()));
506                     startProcessQueue(true);      // start processing the execution queue
507                     dontRedisplay = true;
508                 }
509                 break;
510 
511             case CommandOptions::EDIT_NEW:
512             {
513                 // Edit a new alarm, and optionally preset selected values
514                 if (!initCheck())
515                     exitCode = 1;
516                 else
517                 {
518                     EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow());
519                     if (!editDlg)
520                     {
521                         exitCode = 1;
522                         break;
523                     }
524                     editDlg->setName(options->name());
525                     if (options->alarmTime().isValid())
526                         editDlg->setTime(options->alarmTime());
527                     if (options->recurrence())
528                         editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount());
529                     else if (options->flags() & KAEvent::REPEAT_AT_LOGIN)
530                         editDlg->setRepeatAtLogin();
531                     editDlg->setAction(options->editAction(), AlarmText(options->text()));
532                     if (options->lateCancel())
533                         editDlg->setLateCancel(options->lateCancel());
534                     if (options->flags() & KAEvent::COPY_KORGANIZER)
535                         editDlg->setShowInKOrganizer(true);
536                     switch (options->editType())
537                     {
538                         case EditAlarmDlg::DISPLAY:
539                         {
540                             // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY
541                             auto dlg = qobject_cast<EditDisplayAlarmDlg*>(editDlg);
542                             if (options->fgColour().isValid())
543                                 dlg->setFgColour(options->fgColour());
544                             if (options->bgColour().isValid())
545                                 dlg->setBgColour(options->bgColour());
546                             if (!options->audioFile().isEmpty()
547                             ||  options->flags() & (KAEvent::BEEP | KAEvent::SPEAK))
548                             {
549                                 const KAEvent::Flags flags = options->flags();
550                                 const Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep
551                                                                   : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak
552                                                                   : Preferences::Sound_File;
553                                 dlg->setAudio(type, options->audioFile(), options->audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1));
554                             }
555                             if (options->reminderMinutes())
556                                 dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE));
557                             if (options->flags() & KAEvent::NOTIFY)
558                                 dlg->setNotify(true);
559                             if (options->flags() & KAEvent::CONFIRM_ACK)
560                                 dlg->setConfirmAck(true);
561                             if (options->flags() & KAEvent::AUTO_CLOSE)
562                                 dlg->setAutoClose(true);
563                             break;
564                         }
565                         case EditAlarmDlg::COMMAND:
566                             break;
567                         case EditAlarmDlg::EMAIL:
568                         {
569                             // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL
570                             auto dlg = qobject_cast<EditEmailAlarmDlg*>(editDlg);
571                             if (options->fromID()
572                             ||  !options->addressees().isEmpty()
573                             ||  !options->subject().isEmpty()
574                             ||  !options->attachments().isEmpty())
575                                 dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments());
576                             if (options->flags() & KAEvent::EMAIL_BCC)
577                                 dlg->setBcc(true);
578                             break;
579                         }
580                         case EditAlarmDlg::AUDIO:
581                         {
582                             // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO
583                             auto dlg = qobject_cast<EditAudioAlarmDlg*>(editDlg);
584                             if (!options->audioFile().isEmpty()  ||  options->audioVolume() >= 0)
585                                 dlg->setAudio(options->audioFile(), options->audioVolume());
586                             break;
587                         }
588                         case EditAlarmDlg::NO_TYPE:
589                             break;
590                     }
591 
592                     // Execute the edit dialogue. Note that if no other instance of KAlarm is
593                     // running, this new instance will not exit after the dialogue is closed.
594                     // This is deliberate, since exiting would mean that KAlarm wouldn't
595                     // trigger the new alarm.
596                     KAlarm::execNewAlarmDlg(editDlg);
597 
598                     createOnlyMainWindow();   // prevent the application from quitting
599                 }
600                 break;
601             }
602             case CommandOptions::EDIT_NEW_PRESET:
603                 // Edit a new alarm, preset with a template
604                 if (!initCheck())
605                     exitCode = 1;
606                 else
607                 {
608                     // Execute the edit dialogue. Note that if no other instance of KAlarm is
609                     // running, this new instance will not exit after the dialogue is closed.
610                     // This is deliberate, since exiting would mean that KAlarm wouldn't
611                     // trigger the new alarm.
612                     KAlarm::editNewAlarm(options->name());
613 
614                     createOnlyMainWindow();   // prevent the application from quitting
615                 }
616                 break;
617 
618             case CommandOptions::NEW:
619                 // Display a message or file, execute a command, or send an email
620                 setResourcesTimeout();   // set timeout for resource initialisation
621                 if (!initCheck())
622                     exitCode = 1;
623                 else
624                 {
625                     QueuedAction flags = QueuedAction::CmdLine;
626                     if (!MainWindow::count())
627                         flags = static_cast<QueuedAction>(int(flags) + int(QueuedAction::ErrorExit));
628                     if (!scheduleEvent(flags,
629                                        options->editAction(), options->name(), options->text(),
630                                        options->alarmTime(), options->lateCancel(), options->flags(),
631                                        options->bgColour(), options->fgColour(), QFont(),
632                                        options->audioFile(), options->audioVolume(),
633                                        options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()),
634                                        options->subRepeatInterval(), options->subRepeatCount(),
635                                        options->fromID(), options->addressees(),
636                                        options->subject(), options->attachments()))
637                         exitCode = 1;
638                     else
639                         createOnlyMainWindow();   // prevent the application from quitting
640                 }
641                 break;
642 
643             case CommandOptions::TRAY:
644                 // Display only the system tray icon
645                 if (Preferences::showInSystemTray()  &&  QSystemTrayIcon::isSystemTrayAvailable())
646                 {
647                     if (!initCheck())   // open the calendar, start processing execution queue
648                         exitCode = 1;
649                     else
650                     {
651                         if (!displayTrayIcon(true))
652                             exitCode = 1;
653                     }
654                     break;
655                 }
656                 Q_FALLTHROUGH();   // fall through to NONE
657             case CommandOptions::NONE:
658                 // No arguments - run interactively & display the main window
659 #ifndef NDEBUG
660                 if (options->simulationTime().isValid()  &&  !firstInstance)
661                     break;   // simulating time: don't open main window if already running
662 #endif
663                 if (!initCheck())
664                     exitCode = 1;
665                 else
666                 {
667                     if (mTrayWindow  &&  mTrayWindow->assocMainWindow()  &&  !mTrayWindow->assocMainWindow()->isVisible())
668                         mTrayWindow->showAssocMainWindow();
669                     else
670                     {
671                         MainWindow* win = MainWindow::create();
672                         if (command == CommandOptions::TRAY)
673                             win->setWindowState(win->windowState() | Qt::WindowMinimized);
674                         win->show();
675                     }
676                 }
677                 break;
678             default:
679                 break;
680         }
681     }
682     if (options != CommandOptions::firstInstance())
683         delete options;
684 
685     // If this is the first time through, redisplay any alarm message windows
686     // from last time.
687     if (firstInstance  &&  !dontRedisplay  &&  !exitCode)
688     {
689         /* First time through, so redisplay alarm message windows from last time.
690          * But it is possible for session restoration in some circumstances to
691          * not create any windows, in which case the alarm calendars will have
692          * been deleted - if so, don't try to do anything. (This has been known
693          * to happen under the Xfce desktop.)
694          */
695         if (ResourcesCalendar::instance())
696         {
697             mRedisplayAlarms = true;
698             showRestoredWindows();
699         }
700     }
701 
702     --mActiveCount;
703     firstInstance = false;
704 
705     // Quit the application if this was the last/only running "instance" of the program.
706     // Executing 'return' doesn't work very well since the program continues to
707     // run if no windows were created.
708     if (quitIf(exitCode >= 0 ? exitCode : 0))
709         return exitCode;    // exit this application instance
710 
711     return -1;   // continue executing the application instance
712 }
713 
714 /******************************************************************************
715 * Create a minimised main window if none already exists.
716 * This prevents the application from quitting.
717 */
createOnlyMainWindow()718 void KAlarmApp::createOnlyMainWindow()
719 {
720     if (!MainWindow::count())
721     {
722         if (Preferences::showInSystemTray()  &&  QSystemTrayIcon::isSystemTrayAvailable())
723         {
724             if (displayTrayIcon(true))
725                 return;
726         }
727         MainWindow* win = MainWindow::create();
728         win->setWindowState(Qt::WindowMinimized);
729         win->show();
730     }
731 }
732 
733 /******************************************************************************
734 * Quit the program, optionally only if there are no more "instances" running.
735 * Reply = true if program exited.
736 */
quitIf(int exitCode,bool force)737 bool KAlarmApp::quitIf(int exitCode, bool force)
738 {
739     if (force)
740     {
741         // Quit regardless, except for message windows
742         mQuitting = true;
743         MainWindow::closeAll();
744         mQuitting = false;
745         displayTrayIcon(false);
746         if (MessageDisplay::instanceCount(true))    // ignore always-hidden displays (e.g. audio alarms)
747             return false;
748     }
749     else if (mQuitting)
750         return false;   // MainWindow::closeAll() causes quitIf() to be called again
751     else
752     {
753         // Quit only if there are no more "instances" running
754         mPendingQuit = false;
755         if (mActiveCount > 0  ||  MessageDisplay::instanceCount(true))  // ignore always-hidden displays (e.g. audio alarms)
756             return false;
757         const int mwcount = MainWindow::count();
758         MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr;
759         if (mwcount > 1  ||  (mwcount && (!mw->isHidden() || !mw->isTrayParent())))
760             return false;
761         // There are no windows left except perhaps a main window which is a hidden
762         // tray icon parent, or an always-hidden message window.
763         if (mTrayWindow)
764         {
765             // There is a system tray icon.
766             // Don't exit unless the system tray doesn't seem to exist.
767             if (checkSystemTray())
768                 return false;
769         }
770         if (!mActionQueue.isEmpty()  ||  !mCommandProcesses.isEmpty())
771         {
772             // Don't quit yet if there are outstanding actions on the execution queue
773             mPendingQuit = true;
774             mPendingQuitCode = exitCode;
775             return false;
776         }
777     }
778 
779     // This was the last/only running "instance" of the program, so exit completely.
780     // NOTE: Everything which is terminated/deleted here must where applicable
781     //       be initialised in the initialiseTimerResources() method, in case
782     //       KAlarm is started again before application exit completes!
783     qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting";
784     MessageDisplay::stopAudio(true);
785     if (mCancelRtcWake)
786     {
787         KAlarm::setRtcWakeTime(0, nullptr);
788         KAlarm::deleteRtcWakeConfig();
789     }
790     delete mAlarmTimer;     // prevent checking for alarms after deleting calendars
791     mAlarmTimer = nullptr;
792     mInitialised = false;   // prevent processQueue() from running
793     ResourcesCalendar::terminate();
794     DisplayCalendar::terminate();
795     DataModel::terminate();
796     Q_EMIT setExitValue(exitCode);
797     exit(exitCode);
798     return true;    // sometimes we actually get to here, despite calling exit()
799 }
800 
801 /******************************************************************************
802 * Called when the Quit menu item is selected.
803 * Closes the system tray window and all main windows, but does not exit the
804 * program if other windows are still open.
805 */
doQuit(QWidget * parent)806 void KAlarmApp::doQuit(QWidget* parent)
807 {
808     qCDebug(KALARM_LOG) << "KAlarmApp::doQuit";
809     if (KAMessageBox::warningCancelContinue(parent,
810                                             i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."),
811                                             QString(), KStandardGuiItem::quit(),
812                                             KStandardGuiItem::cancel(), Preferences::QUIT_WARN
813                                            ) != KMessageBox::Continue)
814         return;
815     if (!KAlarm::checkRtcWakeConfig(true).isEmpty())
816     {
817         // A wake-on-suspend alarm is set
818         if (KAMessageBox::warningCancelContinue(parent,
819                                                 i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."),
820                                                 QString(), KStandardGuiItem::quit()
821                                                ) != KMessageBox::Continue)
822             return;
823         mCancelRtcWake = true;
824     }
825     if (!Preferences::autoStart())
826     {
827         int option = KMessageBox::No;
828         if (!Preferences::autoStartChangedByUser())
829         {
830             option = KAMessageBox::questionYesNoCancel(parent,
831                                          xi18nc("@info", "Do you want to start KAlarm at login?<nl/>"
832                                                         "(Note that alarms will be disabled if KAlarm is not started.)"),
833                                          QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(),
834                                          KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START);
835         }
836         switch (option)
837         {
838             case KMessageBox::Yes:
839                 Preferences::setAutoStart(true);
840                 Preferences::setNoAutoStart(false);
841                 break;
842             case KMessageBox::No:
843                 Preferences::setNoAutoStart(true);
844                 break;
845             case KMessageBox::Cancel:
846             default:
847                 return;
848         }
849         Preferences::self()->save();
850     }
851     quitIf(0, true);
852 }
853 
854 /******************************************************************************
855 * Display an error message for a fatal error. Prevent further actions since
856 * the program state is unsafe.
857 */
displayFatalError(const QString & message)858 void KAlarmApp::displayFatalError(const QString& message)
859 {
860     if (!mFatalError)
861     {
862         mFatalError = 1;
863         mFatalMessage = message;
864         if (mInstance)
865             QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
866     }
867 }
868 
869 /******************************************************************************
870 * Quit the program, once the fatal error message has been acknowledged.
871 */
quitFatal()872 void KAlarmApp::quitFatal()
873 {
874     switch (mFatalError)
875     {
876         case 0:
877         case 2:
878             return;
879         case 1:
880             mFatalError = 2;
881             KMessageBox::error(nullptr, mFatalMessage);   // this is an application modal window
882             mFatalError = 3;
883             Q_FALLTHROUGH();   // fall through to '3'
884         case 3:
885             if (mInstance)
886                 mInstance->quitIf(1, true);
887             break;
888     }
889     QTimer::singleShot(1000, this, &KAlarmApp::quitFatal);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
890 }
891 
892 /******************************************************************************
893 * Called by the alarm timer when the next alarm is due.
894 * Also called when the execution queue has finished processing to check for the
895 * next alarm.
896 */
checkNextDueAlarm()897 void KAlarmApp::checkNextDueAlarm()
898 {
899     if (!mAlarmsEnabled)
900         return;
901     // Find the first alarm due
902     KADateTime nextDt;
903     const KAEvent nextEvent = ResourcesCalendar::earliestAlarm(nextDt, mNotificationsInhibited);
904     if (!nextEvent.isValid())
905         return;   // there are no alarms pending
906     const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec());
907     qint64 interval = now.msecsTo(nextDt);
908     qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm: now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval;
909     if (interval <= 0)
910     {
911         // Queue the alarm
912         queueAlarmId(nextEvent);
913         qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent.id() << ": due now";
914         QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
915     }
916     else
917     {
918         // No alarm is due yet, so set timer to wake us when it's due.
919         // Check for integer overflow before setting timer.
920 #ifndef HIBERNATION_SIGNAL
921         /* TODO: Use hibernation wakeup signal:
922          *   #include <Solid/Power>
923          *   connect(Solid::Power::self(), &Solid::Power::resumeFromSuspend, ...)
924          *   (or resumingFromSuspend?)
925          * to be notified when wakeup from hibernation occurs. But can't use it
926          * unless we know that this notification is supported by the system!
927          */
928         /* Re-evaluate the next alarm time every minute, in case the
929          * system clock jumps. The most common case when the clock jumps
930          * is when a laptop wakes from hibernation. If timers were left to
931          * run, they would trigger late by the length of time the system
932          * was asleep.
933          */
934         if (interval > 60000)    // 1 minute
935             interval = 60000;
936 #endif
937         ++interval;    // ensure we don't trigger just before the minute boundary
938         if (interval > INT_MAX)
939             interval = INT_MAX;
940         qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent.id() << "wait" << interval/1000 << "seconds";
941         mAlarmTimer->start(static_cast<int>(interval));
942     }
943 }
944 
945 /******************************************************************************
946 * Called by the alarm timer when the next alarm is due.
947 * Also called when the execution queue has finished processing to check for the
948 * next alarm.
949 */
queueAlarmId(const KAEvent & event)950 void KAlarmApp::queueAlarmId(const KAEvent& event)
951 {
952     const EventId id(event);
953     for (const ActionQEntry& entry : std::as_const(mActionQueue))
954     {
955         if (entry.action == QueuedAction::Handle  &&  entry.eventId == id)
956             return;  // the alarm is already queued
957     }
958     mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, id));
959 }
960 
961 /******************************************************************************
962 * Start processing the execution queue.
963 */
startProcessQueue(bool evenIfStarted)964 void KAlarmApp::startProcessQueue(bool evenIfStarted)
965 {
966     if (!mInitialised  ||  evenIfStarted)
967     {
968         qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue";
969         mInitialised = true;
970         // Process anything already queued.
971         QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
972     }
973 }
974 
975 /******************************************************************************
976 * The main processing loop for KAlarm.
977 * All KAlarm operations involving opening or updating calendar files are called
978 * from this loop to ensure that only one operation is active at any one time.
979 * This precaution is necessary because KAlarm's activities are mostly
980 * asynchronous, being in response to D-Bus calls from other programs or timer
981 * events, any of which can be received in the middle of performing another
982 * operation. If a calendar file is opened or updated while another calendar
983 * operation is in progress, the program has been observed to hang, or the first
984 * calendar call has failed with data loss - clearly unacceptable!!
985 */
processQueue()986 void KAlarmApp::processQueue()
987 {
988     if (mInitialised  &&  !mProcessingQueue)
989     {
990         qCDebug(KALARM_LOG) << "KAlarmApp::processQueue";
991         mProcessingQueue = true;
992 
993         // Refresh alarms if that's been queued
994         KAlarm::refreshAlarmsIfQueued();
995 
996         // Process queued events
997         while (!mActionQueue.isEmpty())
998         {
999             ActionQEntry& entry = mActionQueue.head();
1000 
1001             // If the first action's resource ID is a string, can't process it
1002             // until its numeric resource ID can be found.
1003             if (!entry.resourceId.isEmpty())
1004             {
1005                 if (!Resources::allCreated())
1006                 {
1007                     // If resource population has timed out, discard all queued events.
1008                     if (mResourcesTimedOut)
1009                     {
1010                         qCCritical(KALARM_LOG) << "Error! Timeout creating calendars";
1011                         mActionQueue.clear();
1012                     }
1013                     break;
1014                 }
1015                 // Convert the resource ID string to the numeric resource ID.
1016                 entry.eventId.setResourceId(EventId::getResourceId(entry.resourceId));
1017                 entry.resourceId.clear();
1018             }
1019 
1020             // Can't process the first action until its resource has been populated.
1021             const ResourceId id = entry.eventId.resourceId();
1022             if ((id <  0 && !Resources::allPopulated())
1023             ||  (id >= 0 && !Resources::resource(id).isPopulated()))
1024             {
1025                 // If resource population has timed out, discard all queued events.
1026                 if (mResourcesTimedOut)
1027                 {
1028                     qCCritical(KALARM_LOG) << "Error! Timeout reading calendars";
1029                     mActionQueue.clear();
1030                 }
1031                 break;
1032             }
1033 
1034             // Process the first action in the queue.
1035             const bool findUniqueId   = int(entry.action) & int(QueuedAction::FindId);
1036             bool       exitAfter      = int(entry.action) & int(QueuedAction::Exit);
1037             const bool exitAfterError = int(entry.action) & int(QueuedAction::ErrorExit);
1038             const bool commandLine    = int(entry.action) & int(QueuedAction::CmdLine);
1039             const auto action = static_cast<QueuedAction>(int(entry.action) & int(QueuedAction::ActionMask));
1040 
1041             bool ok = true;
1042             bool inhibit = false;
1043             if (entry.eventId.isEmpty())
1044             {
1045                 // It's a new alarm
1046                 switch (action)
1047                 {
1048                     case QueuedAction::Trigger:
1049                         if (execAlarm(entry.event, entry.event.firstAlarm()) == (void*)-2)
1050                             inhibit = true;
1051                         break;
1052                     case QueuedAction::Handle:
1053                     {
1054                         Resource resource = Resources::destination(CalEvent::ACTIVE, nullptr, Resources::NoResourcePrompt | Resources::UseOnlyResource);
1055                         if (!resource.isValid())
1056                         {
1057                             qCWarning(KALARM_LOG) << "KAlarmApp::processQueue: Error! Cannot create alarm (no default calendar is defined)";
1058 
1059                             if (commandLine)
1060                             {
1061                                 const QString errmsg = xi18nc("@info:shell", "Cannot create alarm: No default calendar is defined");
1062                                 std::cerr << qPrintable(errmsg) << std::endl;
1063                             }
1064                             ok = false;
1065                         }
1066                         else
1067                         {
1068                             const KAlarm::UpdateResult result = KAlarm::addEvent(entry.event, resource, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT);
1069                             if (result >= KAlarm::UPDATE_ERROR)
1070                             {
1071 //TODO: display error message for command line action, but first ensure that one is returned by addEvent()!
1072 #if 0
1073                                 if (commandLine)
1074                                 {
1075                                     const QString errmsg = xi18nc("@info:shell", "Error creating alarm");
1076                                     std::cerr << errmsg.toLocal8Bit().data();
1077                                 }
1078 #endif
1079                                 ok = false;
1080                             }
1081                         }
1082                         if (!ok  &&  exitAfterError)
1083                             exitAfter = true;
1084                         break;
1085                     }
1086                     case QueuedAction::List:
1087                     {
1088                         const QStringList alarms = scheduledAlarmList();
1089                         for (const QString& alarm : alarms)
1090                             std::cout << qUtf8Printable(alarm) << std::endl;
1091                         break;
1092                     }
1093                     default:
1094                         break;
1095                 }
1096             }
1097             else
1098             {
1099                 if (action == QueuedAction::Edit)
1100                 {
1101                     int editingCmdLineAlarm = mEditingCmdLineAlarm & 3;
1102                     bool keepQueued = editingCmdLineAlarm <= 1;
1103                     switch (editingCmdLineAlarm)
1104                     {
1105                         case 0:
1106                             // Initiate editing an alarm specified on the command line.
1107                             mEditingCmdLineAlarm |= 1;
1108                             QTimer::singleShot(0, this, &KAlarmApp::slotEditAlarmById);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1109                             break;
1110                         case 1:
1111                             // Currently editing the alarm.
1112                             break;
1113                         case 2:
1114                             // The edit has completed.
1115                             mEditingCmdLineAlarm = 0;
1116                             break;
1117                         default:
1118                             break;
1119                     }
1120                     if (keepQueued)
1121                         break;
1122                 }
1123                 else
1124                 {
1125                     // Trigger the event if it's due.
1126                     const int result = handleEvent(entry.eventId, action, findUniqueId);
1127                     if (!result)
1128                         inhibit = true;
1129                     else if (result < 0  &&  exitAfter)
1130                     {
1131                         CommandOptions::printError(xi18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not unique", mCommandOption, entry.eventId.eventId()));
1132                         ok = false;
1133                     }
1134                 }
1135             }
1136 
1137             mActionQueue.dequeue();
1138 
1139             if (inhibit)
1140             {
1141                 // It's a display event which can't be executed because notifications
1142                 // are inhibited. Move it to the inhibited queue until the inhibition
1143                 // is removed.
1144             }
1145             else if (exitAfter)
1146             {
1147                 mProcessingQueue = false;   // don't inhibit processing if there is another instance
1148                 quitIf((ok ? 0 : 1), exitAfterError);
1149                 return;  // quitIf() can sometimes return, despite calling exit()
1150             }
1151         }
1152 
1153         // Purge the default archived alarms resource if it's time to do so
1154         if (mPurgeDaysQueued >= 0)
1155         {
1156             KAlarm::purgeArchive(mPurgeDaysQueued);
1157             mPurgeDaysQueued = -1;
1158         }
1159 
1160         // Now that the queue has been processed, quit if a quit was queued
1161         if (mPendingQuit)
1162         {
1163             if (quitIf(mPendingQuitCode))
1164                 return;  // quitIf() can sometimes return, despite calling exit()
1165         }
1166 
1167         mProcessingQueue = false;
1168 
1169         if (!mEditingCmdLineAlarm)
1170         {
1171             // Schedule the application to be woken when the next alarm is due
1172             checkNextDueAlarm();
1173         }
1174     }
1175 }
1176 
1177 /******************************************************************************
1178 * Called when a repeat-at-login alarm has been added externally.
1179 * Queues the alarm for triggering.
1180 * First, cancel any scheduled reminder or deferral for it, since these will be
1181 * superseded by the new at-login trigger.
1182 */
atLoginEventAdded(const KAEvent & event)1183 void KAlarmApp::atLoginEventAdded(const KAEvent& event)
1184 {
1185     KAEvent ev = event;
1186     if (!cancelReminderAndDeferral(ev))
1187     {
1188         if (mAlarmsEnabled)
1189         {
1190             mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, EventId(ev)));
1191             if (mInitialised)
1192                 QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1193         }
1194     }
1195 }
1196 
1197 /******************************************************************************
1198 * Called when the system tray main window is closed.
1199 */
removeWindow(TrayWindow *)1200 void KAlarmApp::removeWindow(TrayWindow*)
1201 {
1202     mTrayWindow = nullptr;
1203 }
1204 
1205 /******************************************************************************
1206 * Display or close the system tray icon.
1207 */
displayTrayIcon(bool show,MainWindow * parent)1208 bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent)
1209 {
1210     qCDebug(KALARM_LOG) << "KAlarmApp::displayTrayIcon";
1211     static bool creating = false;
1212     if (show)
1213     {
1214         if (!mTrayWindow  &&  !creating)
1215         {
1216             if (!QSystemTrayIcon::isSystemTrayAvailable())
1217                 return false;
1218             if (!MainWindow::count())
1219             {
1220                 // We have to have at least one main window to act
1221                 // as parent to the system tray icon (even if the
1222                 // window is hidden).
1223                 creating = true;    // prevent main window constructor from creating an additional tray icon
1224                 parent = MainWindow::create();
1225                 creating = false;
1226             }
1227             mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow());
1228             connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled);
1229             Q_EMIT trayIconToggled();
1230 
1231             if (!checkSystemTray())
1232                 quitIf(0);    // exit the application if there are no open windows
1233         }
1234     }
1235     else
1236     {
1237         delete mTrayWindow;
1238         mTrayWindow = nullptr;
1239     }
1240     return true;
1241 }
1242 
1243 /******************************************************************************
1244 * Check whether the system tray icon has been housed in the system tray.
1245 */
checkSystemTray()1246 bool KAlarmApp::checkSystemTray()
1247 {
1248     if (!mTrayWindow)
1249         return true;
1250     if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray)
1251     {
1252         qCDebug(KALARM_LOG) << "KAlarmApp::checkSystemTray: changed ->" << mNoSystemTray;
1253         mNoSystemTray = !mNoSystemTray;
1254 
1255         // Store the new setting in the config file, so that if KAlarm exits it will
1256         // restart with the correct default.
1257         KConfigGroup config(KSharedConfig::openConfig(), "General");
1258         config.writeEntry("NoSystemTray", mNoSystemTray);
1259         config.sync();
1260 
1261         // Update other settings
1262         slotShowInSystemTrayChanged();
1263     }
1264     return !mNoSystemTray;
1265 }
1266 
1267 /******************************************************************************
1268 * Return the main window associated with the system tray icon.
1269 */
trayMainWindow() const1270 MainWindow* KAlarmApp::trayMainWindow() const
1271 {
1272     return mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr;
1273 }
1274 
1275 /******************************************************************************
1276 * Called when the show-in-system-tray preference setting has changed, to show
1277 * or hide the system tray icon.
1278 */
slotShowInSystemTrayChanged()1279 void KAlarmApp::slotShowInSystemTrayChanged()
1280 {
1281     const bool newShowInSysTray = wantShowInSystemTray();
1282     if (newShowInSysTray != mOldShowInSystemTray)
1283     {
1284         // The system tray run mode has changed
1285         ++mActiveCount;         // prevent the application from quitting
1286         MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr;
1287         delete mTrayWindow;     // remove the system tray icon if it is currently shown
1288         mTrayWindow = nullptr;
1289         mOldShowInSystemTray = newShowInSysTray;
1290         if (newShowInSysTray)
1291         {
1292             // Show the system tray icon
1293             displayTrayIcon(true);
1294         }
1295         else
1296         {
1297             // Stop showing the system tray icon
1298             if (win  &&  win->isHidden())
1299             {
1300                 if (MainWindow::count() > 1)
1301                     delete win;
1302                 else
1303                 {
1304                     win->setWindowState(win->windowState() | Qt::WindowMinimized);
1305                     win->show();
1306                 }
1307             }
1308         }
1309         --mActiveCount;
1310     }
1311 }
1312 
1313 /******************************************************************************
1314 * Called when the start-of-day time preference setting has changed.
1315 * Change alarm times for date-only alarms.
1316 */
changeStartOfDay()1317 void KAlarmApp::changeStartOfDay()
1318 {
1319     DateTime::setStartOfDay(Preferences::startOfDay());
1320     KAEvent::setStartOfDay(Preferences::startOfDay());
1321     Resources::adjustStartOfDay();
1322     DisplayCalendar::adjustStartOfDay();
1323 }
1324 
1325 /******************************************************************************
1326 * Called when the default alarm message font preference setting has changed.
1327 * Notify KAEvent.
1328 */
slotMessageFontChanged(const QFont & font)1329 void KAlarmApp::slotMessageFontChanged(const QFont& font)
1330 {
1331     KAEvent::setDefaultFont(font);
1332 }
1333 
1334 /******************************************************************************
1335 * Called when the working time preference settings have changed.
1336 * Notify KAEvent.
1337 */
slotWorkTimeChanged(const QTime & start,const QTime & end,const QBitArray & days)1338 void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days)
1339 {
1340     KAEvent::setWorkTime(days, start, end);
1341 }
1342 
1343 /******************************************************************************
1344 * Called when the holiday region preference setting has changed.
1345 * Notify KAEvent.
1346 */
slotHolidaysChanged(const KHolidays::HolidayRegion & holidays)1347 void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays)
1348 {
1349     KAEvent::setHolidays(holidays);
1350 }
1351 
1352 /******************************************************************************
1353 * Called when the date for February 29th recurrences has changed in the
1354 * preferences settings.
1355 */
slotFeb29TypeChanged(Preferences::Feb29Type type)1356 void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type)
1357 {
1358     KARecurrence::Feb29Type rtype;
1359     switch (type)
1360     {
1361         default:
1362         case Preferences::Feb29_None:   rtype = KARecurrence::Feb29_None;  break;
1363         case Preferences::Feb29_Feb28:  rtype = KARecurrence::Feb29_Feb28;  break;
1364         case Preferences::Feb29_Mar1:   rtype = KARecurrence::Feb29_Mar1;  break;
1365     }
1366     KARecurrence::setDefaultFeb29Type(rtype);
1367 }
1368 
1369 /******************************************************************************
1370 * Return whether the program is configured to be running in the system tray.
1371 */
wantShowInSystemTray() const1372 bool KAlarmApp::wantShowInSystemTray() const
1373 {
1374     return Preferences::showInSystemTray()  &&  QSystemTrayIcon::isSystemTrayAvailable();
1375 }
1376 
1377 /******************************************************************************
1378 * Set a timeout for populating resources.
1379 */
setResourcesTimeout()1380 void KAlarmApp::setResourcesTimeout()
1381 {
1382     QTimer::singleShot(RESOURCES_TIMEOUT * 1000, this, &KAlarmApp::slotResourcesTimeout);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1383 }
1384 
1385 /******************************************************************************
1386 * Called on a timeout to check whether resources have been populated.
1387 * If not, exit the program with code 1.
1388 */
slotResourcesTimeout()1389 void KAlarmApp::slotResourcesTimeout()
1390 {
1391     if (!Resources::allPopulated())
1392     {
1393         // Resource population has timed out.
1394         mResourcesTimedOut = true;
1395         quitIf(1);
1396     }
1397 }
1398 
1399 /******************************************************************************
1400 * Called when all resources have been created at startup.
1401 * Check whether there are any writable active calendars, and if not, warn the
1402 * user.
1403 * If alarms are being archived, check whether there is a default archived
1404 * calendar, and if not, warn the user.
1405 */
slotResourcesCreated()1406 void KAlarmApp::slotResourcesCreated()
1407 {
1408     showRestoredWindows();   // display message windows restored at startup.
1409     checkWritableCalendar();
1410     checkArchivedCalendar();
1411     processQueue();
1412 }
1413 
1414 /******************************************************************************
1415 * Called when all calendars have been fetched at startup, or calendar migration
1416 * has completed.
1417 * Check whether there are any writable active calendars, and if not, warn the
1418 * user.
1419 */
checkWritableCalendar()1420 void KAlarmApp::checkWritableCalendar()
1421 {
1422     if (mReadOnly)
1423         return;    // don't need write access to calendars
1424     if (!Resources::allCreated()
1425     ||  !DataModel::isMigrationComplete())
1426         return;
1427     static bool done = false;
1428     if (done)
1429         return;
1430     done = true;
1431     qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar";
1432 
1433     // Check for, and remove, any duplicate resources, i.e. those which use the
1434     // same calendar file/directory.
1435     DataModel::removeDuplicateResources();
1436 
1437     // Find whether there are any writable active alarm calendars
1438     const bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty();
1439     if (!active)
1440     {
1441         qCWarning(KALARM_LOG) << "KAlarmApp::checkWritableCalendar: No writable active calendar";
1442         KAMessageBox::information(MainWindow::mainMainWindow(),
1443                                   xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled.<nl/><nl/>"
1444                                                  "To fix this, use <interface>View | Show Calendars</interface> to check or change calendar statuses."),
1445                                   QString(), QStringLiteral("noWritableCal"));
1446     }
1447 }
1448 
1449 /******************************************************************************
1450 * If alarms are being archived, check whether there is a default archived
1451 * calendar, and if not, warn the user.
1452 */
checkArchivedCalendar()1453 void KAlarmApp::checkArchivedCalendar()
1454 {
1455     static bool done = false;
1456     if (done)
1457         return;
1458     done = true;
1459 
1460     // If alarms are to be archived, check that the default archived alarm
1461     // calendar is writable.
1462     if (Preferences::archivedKeepDays())
1463     {
1464         Resource standard = Resources::getStandard(CalEvent::ARCHIVED, true);
1465         if (!standard.isValid())
1466         {
1467             // Schedule the display of a user prompt, without holding up
1468             // other processing.
1469             QTimer::singleShot(0, this, &KAlarmApp::promptArchivedCalendar);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1470         }
1471     }
1472 }
1473 
1474 /******************************************************************************
1475 * Edit an alarm specified on the command line.
1476 */
slotEditAlarmById()1477 void KAlarmApp::slotEditAlarmById()
1478 {
1479     qCDebug(KALARM_LOG) << "KAlarmApp::slotEditAlarmById";
1480     ActionQEntry& entry = mActionQueue.head();
1481     if (!KAlarm::editAlarmById(entry.eventId))
1482     {
1483         CommandOptions::printError(xi18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not editable", mCommandOption, entry.eventId.eventId()));
1484         mActionQueue.clear();
1485         quitIf(1);
1486     }
1487     else
1488     {
1489         createOnlyMainWindow();    // prevent the application from quitting
1490         if (mEditingCmdLineAlarm & 0x10)
1491         {
1492             mRedisplayAlarms = true;
1493             showRestoredWindows();
1494         }
1495         mEditingCmdLineAlarm = 2;  // indicate edit completion
1496         QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1497     }
1498 }
1499 
1500 /******************************************************************************
1501 * If alarms are being archived, check whether there is a default archived
1502 * calendar, and if not, warn the user.
1503 */
promptArchivedCalendar()1504 void KAlarmApp::promptArchivedCalendar()
1505 {
1506     const bool archived = !Resources::enabledResources(CalEvent::ARCHIVED, true).isEmpty();
1507     if (archived)
1508     {
1509         qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no writable archived calendar";
1510         KAMessageBox::information(MainWindow::mainMainWindow(),
1511                                   xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no writable archived alarm calendar is enabled.<nl/><nl/>"
1512                                                  "To fix this, use <interface>View | Show Calendars</interface> to check or change calendar statuses."),
1513                                   QString(), QStringLiteral("noWritableArch"));
1514     }
1515     else
1516     {
1517         qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no standard archived calendar";
1518         KAMessageBox::information(MainWindow::mainMainWindow(),
1519                                   xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no archived alarm calendar is set as default.<nl/><nl/>"
1520                                                  "To fix this, use <interface>View | Show Calendars</interface>, select an archived alarms calendar, and check <interface>Use as Default for Archived Alarms</interface>."),
1521                                   QString(), QStringLiteral("noStandardArch"));
1522     }
1523 }
1524 
1525 /******************************************************************************
1526 * Called when a new resource has been added, to note the possible need to purge
1527 * its old alarms if it is the default archived calendar.
1528 */
slotResourceAdded(const Resource & resource)1529 void KAlarmApp::slotResourceAdded(const Resource& resource)
1530 {
1531     if (resource.alarmTypes() & CalEvent::ARCHIVED)
1532         mPendingPurges += resource.id();
1533 }
1534 
1535 /******************************************************************************
1536 * Called when a resource has been populated, to purge its old alarms if it is
1537 * the default archived calendar.
1538 */
slotResourcePopulated(const Resource & resource)1539 void KAlarmApp::slotResourcePopulated(const Resource& resource)
1540 {
1541     if (mPendingPurges.removeAll(resource.id()) > 0)
1542         purgeNewArchivedDefault(resource);
1543 }
1544 
1545 /******************************************************************************
1546 * Called when a new resource has been populated, or when a resource has been
1547 * set as the standard resource for its type.
1548 * If it is the default archived calendar, purge its old alarms if necessary.
1549 */
purgeNewArchivedDefault(const Resource & resource)1550 void KAlarmApp::purgeNewArchivedDefault(const Resource& resource)
1551 {
1552     if (Resources::isStandard(resource, CalEvent::ARCHIVED))
1553     {
1554         qCDebug(KALARM_LOG) << "KAlarmApp::purgeNewArchivedDefault:" << resource.displayId() << ": standard archived...";
1555         if (mArchivedPurgeDays >= 0)
1556             purge(mArchivedPurgeDays);
1557         else
1558             setArchivePurgeDays();
1559     }
1560 }
1561 
1562 /******************************************************************************
1563 * Called when the length of time to keep archived alarms changes in KAlarm's
1564 * preferences.
1565 * Set the number of days to keep archived alarms.
1566 * Alarms which are older are purged immediately, and at the start of each day.
1567 */
setArchivePurgeDays()1568 void KAlarmApp::setArchivePurgeDays()
1569 {
1570     const int newDays = Preferences::archivedKeepDays();
1571     if (newDays != mArchivedPurgeDays)
1572     {
1573         const int oldDays = mArchivedPurgeDays;
1574         mArchivedPurgeDays = newDays;
1575         if (mArchivedPurgeDays <= 0)
1576             StartOfDayTimer::disconnect(this);
1577         if (mArchivedPurgeDays < 0)
1578             return;   // keep indefinitely, so don't purge
1579         if (oldDays < 0  ||  mArchivedPurgeDays < oldDays)
1580         {
1581             // Alarms are now being kept for less long, so purge them
1582             purge(mArchivedPurgeDays);
1583             if (!mArchivedPurgeDays)
1584                 return;   // don't archive any alarms
1585         }
1586         // Start the purge timer to expire at the start of the next day
1587         // (using the user-defined start-of-day time).
1588         StartOfDayTimer::connect(this, SLOT(slotPurge()));
1589     }
1590 }
1591 
1592 /******************************************************************************
1593 * Purge all archived events from the calendar whose end time is longer ago than
1594 * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero.
1595 */
purge(int daysToKeep)1596 void KAlarmApp::purge(int daysToKeep)
1597 {
1598     if (mPurgeDaysQueued < 0  ||  daysToKeep < mPurgeDaysQueued)
1599         mPurgeDaysQueued = daysToKeep;
1600 
1601     // Do the purge once any other current operations are completed
1602     processQueue();
1603 }
1604 
1605 /******************************************************************************
1606 * Called when the Freedesktop notifications properties have changed.
1607 * Check whether the inhibited property has changed.
1608 */
slotFDOPropertiesChanged(const QString & interface,const QVariantMap & changedProperties,const QStringList & invalidatedProperties)1609 void KAlarmApp::slotFDOPropertiesChanged(const QString& interface,
1610                                          const QVariantMap& changedProperties,
1611                                          const QStringList& invalidatedProperties)
1612 {
1613     Q_UNUSED(interface);     // always "org.freedesktop.Notifications"
1614     Q_UNUSED(invalidatedProperties);
1615     const auto it = changedProperties.find(QStringLiteral("Inhibited"));
1616     if (it != changedProperties.end())
1617     {
1618         const bool inhibited = it.value().toBool();
1619         if (inhibited != mNotificationsInhibited)
1620         {
1621             qCDebug(KALARM_LOG) << "KAlarmApp::slotFDOPropertiesChanged: Notifications inhibited ->" << inhibited;
1622             mNotificationsInhibited = inhibited;
1623             if (!mNotificationsInhibited)
1624             {
1625                 showRestoredWindows();   // display message windows restored at startup.
1626                 QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1627             }
1628         }
1629     }
1630 }
1631 
1632 /******************************************************************************
1633 * Output a list of pending alarms, with their next scheduled occurrence.
1634 */
scheduledAlarmList()1635 QStringList KAlarmApp::scheduledAlarmList()
1636 {
1637     QStringList alarms;
1638     const QVector<KAEvent> events = KAlarm::getSortedActiveEvents(this);
1639     for (const KAEvent& event : events)
1640     {
1641         const KADateTime dateTime = event.nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone();
1642         const Resource resource = Resources::resource(event.resourceId());
1643         QString text(resource.configName() + QLatin1String(":"));
1644         text += event.id() + QLatin1Char(' ')
1645              +  dateTime.toString(QStringLiteral("%Y%m%dT%H%M "))
1646              +  AlarmText::summary(event, 1);
1647         alarms << text;
1648     }
1649     return alarms;
1650 }
1651 
1652 /******************************************************************************
1653 * Enable or disable alarm monitoring.
1654 */
setAlarmsEnabled(bool enabled)1655 void KAlarmApp::setAlarmsEnabled(bool enabled)
1656 {
1657     if (enabled != mAlarmsEnabled)
1658     {
1659         mAlarmsEnabled = enabled;
1660         Q_EMIT alarmEnabledToggled(enabled);
1661         if (!enabled)
1662             KAlarm::cancelRtcWake(nullptr);
1663         else if (!mProcessingQueue)
1664             checkNextDueAlarm();
1665     }
1666 }
1667 
1668 /******************************************************************************
1669 * Spread or collect alarm message and error message windows.
1670 */
spreadWindows(bool spread)1671 void KAlarmApp::spreadWindows(bool spread)
1672 {
1673     spread = MessageWindow::spread(spread);
1674     Q_EMIT spreadWindowsToggled(spread);
1675 }
1676 
1677 /******************************************************************************
1678 * Called when the spread status of message windows changes.
1679 * Set the 'spread windows' action state.
1680 */
setSpreadWindowsState(bool spread)1681 void KAlarmApp::setSpreadWindowsState(bool spread)
1682 {
1683     Q_EMIT spreadWindowsToggled(spread);
1684 }
1685 
1686 /******************************************************************************
1687 * Check whether the window manager's handling of keyboard focus transfer
1688 * between application windows is broken. This is true for Ubuntu's Unity
1689 * desktop, where MessageWindow windows steal keyboard focus from EditAlarmDlg
1690 * windows.
1691 */
windowFocusBroken() const1692 bool KAlarmApp::windowFocusBroken() const
1693 {
1694     return mWindowFocusBroken;
1695 }
1696 
1697 /******************************************************************************
1698 * Check whether window/keyboard focus currently needs to be fixed manually due
1699 * to the window manager not handling it correctly. This will occur if there are
1700 * both EditAlarmDlg and MessageWindow windows currently active.
1701 */
needWindowFocusFix() const1702 bool KAlarmApp::needWindowFocusFix() const
1703 {
1704     return mWindowFocusBroken && MessageWindow::windowCount(true) && EditAlarmDlg::instanceCount();
1705 }
1706 
1707 /******************************************************************************
1708 * Called to schedule a new alarm, either in response to a D-Bus notification or
1709 * to command line options.
1710 * Reply = true unless there was a parameter error or an error opening calendar file.
1711 */
scheduleEvent(QueuedAction queuedActionFlags,KAEvent::SubAction action,const QString & name,const QString & text,const KADateTime & dateTime,int lateCancel,KAEvent::Flags flags,const QColor & bg,const QColor & fg,const QFont & font,const QString & audioFile,float audioVolume,int reminderMinutes,const KARecurrence & recurrence,const KCalendarCore::Duration & repeatInterval,int repeatCount,uint mailFromID,const KCalendarCore::Person::List & mailAddresses,const QString & mailSubject,const QStringList & mailAttachments)1712 bool KAlarmApp::scheduleEvent(QueuedAction queuedActionFlags,
1713                               KAEvent::SubAction action, const QString& name, const QString& text,
1714                               const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags,
1715                               const QColor& bg, const QColor& fg, const QFont& font,
1716                               const QString& audioFile, float audioVolume, int reminderMinutes,
1717                               const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount,
1718                               uint mailFromID, const KCalendarCore::Person::List& mailAddresses,
1719                               const QString& mailSubject, const QStringList& mailAttachments)
1720 {
1721     if (!dateTime.isValid())
1722     {
1723         qCWarning(KALARM_LOG) << "KAlarmApp::scheduleEvent: Error! Invalid time" << text;
1724         return false;
1725     }
1726     const KADateTime now = KADateTime::currentUtcDateTime();
1727     if (lateCancel  &&  dateTime < now.addSecs(-maxLateness(lateCancel)))
1728     {
1729         qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: not executed (late-cancel)" << text;
1730         return true;               // alarm time was already archived too long ago
1731     }
1732     KADateTime alarmTime = dateTime;
1733     // Round down to the nearest minute to avoid scheduling being messed up
1734     if (!dateTime.isDateOnly())
1735         alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0));
1736 
1737     KAEvent event(alarmTime, name, text, bg, fg, font, action, lateCancel, flags, true);
1738     if (reminderMinutes)
1739     {
1740         const bool onceOnly = flags & KAEvent::REMINDER_ONCE;
1741         event.setReminder(reminderMinutes, onceOnly);
1742     }
1743     if (!audioFile.isEmpty())
1744         event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1);
1745     if (!mailAddresses.isEmpty())
1746         event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments);
1747     event.setRecurrence(recurrence);
1748     event.setFirstRecurrence();
1749     event.setRepetition(Repetition(repeatInterval, repeatCount - 1));
1750     event.endChanges();
1751     if (alarmTime <= now)
1752     {
1753         // Alarm is due for execution already.
1754         // First execute it once without adding it to the calendar file.
1755         qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: executing" << text;
1756         if (!mInitialised
1757         ||  execAlarm(event, event.firstAlarm()) == (void*)-2)
1758             mActionQueue.enqueue(ActionQEntry(event, QueuedAction::Trigger));
1759         // If it's a recurring alarm, reschedule it for its next occurrence
1760         if (!event.recurs()
1761         ||  event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE)
1762             return true;
1763         // It has recurrences in the future
1764     }
1765 
1766     // Queue the alarm for insertion into the calendar file
1767     qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: creating new alarm" << text;
1768     const QueuedAction qaction = static_cast<QueuedAction>(int(QueuedAction::Handle) + int(queuedActionFlags));
1769     mActionQueue.enqueue(ActionQEntry(event, qaction));
1770     if (mInitialised)
1771         QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1772     return true;
1773 }
1774 
1775 /******************************************************************************
1776 * Called in response to a D-Bus request to trigger or cancel an event.
1777 * Optionally display the event. Delete the event from the calendar file and
1778 * from every main window instance.
1779 */
dbusHandleEvent(const EventId & eventID,QueuedAction action)1780 bool KAlarmApp::dbusHandleEvent(const EventId& eventID, QueuedAction action)
1781 {
1782     qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << eventID;
1783     mActionQueue.append(ActionQEntry(action, eventID));
1784     if (mInitialised)
1785         QTimer::singleShot(0, this, &KAlarmApp::processQueue);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1786     return true;
1787 }
1788 
1789 /******************************************************************************
1790 * Called in response to a D-Bus request to list all pending alarms.
1791 */
dbusList()1792 QString KAlarmApp::dbusList()
1793 {
1794     qCDebug(KALARM_LOG) << "KAlarmApp::dbusList";
1795     return scheduledAlarmList().join(QLatin1Char('\n')) + QLatin1Char('\n');
1796 }
1797 
1798 /******************************************************************************
1799 * Either:
1800 * a) Execute the event if it's due, and then delete it if it has no outstanding
1801 *    repetitions.
1802 * b) Delete the event.
1803 * c) Reschedule the event for its next repetition. If none remain, delete it.
1804 * If the event is deleted, it is removed from the calendar file and from every
1805 * main window instance.
1806 * If 'findUniqueId' is true and 'id' does not specify a resource, all resources
1807 * will be searched for the event's unique ID.
1808 * Reply = -1 if event ID not found, or if more than one event with the same ID
1809 *            is found.
1810 *       =  0 if can't trigger display event because notifications are inhibited.
1811 *       =  1 if success.
1812 */
handleEvent(const EventId & id,QueuedAction action,bool findUniqueId)1813 int KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniqueId)
1814 {
1815     Q_ASSERT(!(int(action) & ~int(QueuedAction::ActionMask)));
1816 
1817     // Delete any expired wake-on-suspend config data
1818     KAlarm::checkRtcWakeConfig();
1819 
1820     const QString eventID(id.eventId());
1821     KAEvent event = ResourcesCalendar::event(id, findUniqueId);
1822     if (!event.isValid())
1823     {
1824         if (id.resourceId() != -1)
1825             qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID;
1826         else if (findUniqueId)
1827             qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID;
1828         else
1829             qCCritical(KALARM_LOG) << "KAlarmApp::handleEvent: No resource ID specified for event:" << eventID;
1830         return -1;
1831     }
1832     switch (action)
1833     {
1834         case QueuedAction::Cancel:
1835         {
1836             qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL";
1837             Resource resource = Resources::resource(event.resourceId());
1838             KAlarm::deleteEvent(event, resource, true);
1839             break;
1840         }
1841         case QueuedAction::Trigger:    // handle it if it's due, else execute it regardless
1842         case QueuedAction::Handle:     // handle it if it's due
1843         {
1844             const KADateTime now = KADateTime::currentUtcDateTime();
1845             qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (action==QueuedAction::Trigger?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC";
1846             bool updateCalAndDisplay = false;
1847             bool alarmToExecuteValid = false;
1848             KAAlarm alarmToExecute;
1849             bool restart = false;
1850             // Check all the alarms in turn.
1851             // Note that the main alarm is fetched before any other alarms.
1852             for (KAAlarm alarm = event.firstAlarm();
1853                  alarm.isValid();
1854                  alarm = (restart ? event.firstAlarm() : event.nextAlarm(alarm)), restart = false)
1855             {
1856                 // Check if the alarm is due yet.
1857                 const KADateTime nextDT = alarm.dateTime(true).effectiveKDateTime();
1858                 const int secs = nextDT.secsTo(now);
1859                 if (secs < 0)
1860                 {
1861                     // The alarm appears to be in the future.
1862                     // Check if it's an invalid local time during a daylight
1863                     // saving time shift, which has actually passed.
1864                     if (alarm.dateTime().timeSpec() != KADateTime::LocalZone
1865                     ||  nextDT > now.toTimeSpec(KADateTime::LocalZone))
1866                     {
1867                         // This alarm is definitely not due yet
1868                         qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due";
1869                         continue;
1870                     }
1871                 }
1872                 bool reschedule = false;
1873                 bool rescheduleWork = false;
1874                 if ((event.workTimeOnly() || event.holidaysExcluded())  &&  !alarm.deferred())
1875                 {
1876                     // The alarm is restricted to working hours and/or non-holidays
1877                     // (apart from deferrals). This needs to be re-evaluated every
1878                     // time it triggers, since working hours could change.
1879                     if (alarm.dateTime().isDateOnly())
1880                     {
1881                         KADateTime dt(nextDT);
1882                         dt.setDateOnly(true);
1883                         reschedule = event.excludedByWorkTimeOrHoliday(dt);
1884                     }
1885                     else
1886                         reschedule = event.excludedByWorkTimeOrHoliday(nextDT);
1887                     rescheduleWork = reschedule;
1888                     if (reschedule)
1889                         qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours";
1890                 }
1891                 if (!reschedule  &&  alarm.repeatAtLogin())
1892                 {
1893                     // Alarm is to be displayed at every login.
1894                     qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: REPEAT_AT_LOGIN";
1895                     // Check if the main alarm is already being displayed.
1896                     // (We don't want to display both at the same time.)
1897                     if (alarmToExecute.isValid())
1898                         continue;
1899 
1900                     // Set the time to display if it's a display alarm
1901                     alarm.setTime(now);
1902                 }
1903                 if (!reschedule  &&  event.lateCancel())
1904                 {
1905                     // Alarm is due, and it is to be cancelled if too late.
1906                     qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: LATE_CANCEL";
1907                     bool cancel = false;
1908                     if (alarm.dateTime().isDateOnly())
1909                     {
1910                         // The alarm has no time, so cancel it if its date is too far past
1911                         const int maxlate = event.lateCancel() / 1440;    // maximum lateness in days
1912                         KADateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime());
1913                         if (now >= limit)
1914                         {
1915                             // It's too late to display the scheduled occurrence.
1916                             // Find the last previous occurrence of the alarm.
1917                             DateTime next;
1918                             const KAEvent::OccurType type = event.previousOccurrence(now, next, true);
1919                             switch (type & ~KAEvent::OCCURRENCE_REPEAT)
1920                             {
1921                                 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
1922                                 case KAEvent::RECURRENCE_DATE:
1923                                 case KAEvent::RECURRENCE_DATE_TIME:
1924                                 case KAEvent::LAST_RECURRENCE:
1925                                     limit.setDate(next.date().addDays(maxlate + 1));
1926                                     if (now >= limit)
1927                                     {
1928                                         if (type == KAEvent::LAST_RECURRENCE
1929                                         ||  (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event.recurs()))
1930                                             cancel = true;   // last occurrence (and there are no repetitions)
1931                                         else
1932                                             reschedule = true;
1933                                     }
1934                                     break;
1935                                 case KAEvent::NO_OCCURRENCE:
1936                                 default:
1937                                     reschedule = true;
1938                                     break;
1939                             }
1940                         }
1941                     }
1942                     else
1943                     {
1944                         // The alarm is timed. Allow it to be the permitted amount late before cancelling it.
1945                         const int maxlate = maxLateness(event.lateCancel());
1946                         if (secs > maxlate)
1947                         {
1948                             // It's over the maximum interval late.
1949                             // Find the most recent occurrence of the alarm.
1950                             DateTime next;
1951                             const KAEvent::OccurType type = event.previousOccurrence(now, next, true);
1952                             switch (type & ~KAEvent::OCCURRENCE_REPEAT)
1953                             {
1954                                 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
1955                                 case KAEvent::RECURRENCE_DATE:
1956                                 case KAEvent::RECURRENCE_DATE_TIME:
1957                                 case KAEvent::LAST_RECURRENCE:
1958                                     if (next.effectiveKDateTime().secsTo(now) > maxlate)
1959                                     {
1960                                         if (type == KAEvent::LAST_RECURRENCE
1961                                         ||  (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event.recurs()))
1962                                             cancel = true;   // last occurrence (and there are no repetitions)
1963                                         else
1964                                             reschedule = true;
1965                                     }
1966                                     break;
1967                                 case KAEvent::NO_OCCURRENCE:
1968                                 default:
1969                                     reschedule = true;
1970                                     break;
1971                             }
1972                         }
1973                     }
1974 
1975                     if (cancel)
1976                     {
1977                         // All recurrences are finished, so cancel the event
1978                         event.setArchive();
1979                         if (cancelAlarm(event, alarm.type(), false))
1980                             return 1;   // event has been deleted
1981                         updateCalAndDisplay = true;
1982                         continue;
1983                     }
1984                 }
1985                 if (reschedule)
1986                 {
1987                     // The latest repetition was too long ago, so schedule the next one
1988                     switch (rescheduleAlarm(event, alarm, false, (rescheduleWork ? nextDT : KADateTime())))
1989                     {
1990                         case 1:
1991                             // A working-time-only alarm has been rescheduled and the
1992                             // rescheduled time is already due. Start processing the
1993                             // event again.
1994                             alarmToExecuteValid = false;
1995                             restart = true;
1996                             break;
1997                         case -1:
1998                             return 1;   // event has been deleted
1999                         default:
2000                             break;
2001                     }
2002                     updateCalAndDisplay = true;
2003                     continue;
2004                 }
2005                 if (!alarmToExecuteValid)
2006                 {
2007                     qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": execute";
2008                     alarmToExecute = alarm;             // note the alarm to be displayed
2009                     alarmToExecuteValid = true;         // only trigger one alarm for the event
2010                 }
2011                 else
2012                     qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": skip";
2013             }
2014 
2015             // If there is an alarm to execute, do this last after rescheduling/cancelling
2016             // any others. This ensures that the updated event is only saved once to the calendar.
2017             if (alarmToExecute.isValid())
2018             {
2019                 if (execAlarm(event, alarmToExecute, Reschedule | (alarmToExecute.repeatAtLogin() ? NoExecFlag : AllowDefer)) == (void*)-2)
2020                     return 0;    // display alarm, but notifications are inhibited
2021             }
2022             else
2023             {
2024                 if (action == QueuedAction::Trigger)
2025                 {
2026                     // The alarm is to be executed regardless of whether it's due.
2027                     // Only trigger one alarm from the event - we don't want multiple
2028                     // identical messages, for example.
2029                     const KAAlarm alarm = event.firstAlarm();
2030                     if (alarm.isValid())
2031                     {
2032                         if (execAlarm(event, alarm) == (void*)-2)
2033                             return 0;    // display alarm, but notifications are inhibited
2034                     }
2035                 }
2036                 if (updateCalAndDisplay)
2037                     KAlarm::updateEvent(event);     // update the window lists and calendar file
2038                 else if (action != QueuedAction::Trigger) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; }
2039             }
2040             break;
2041         }
2042         default:
2043             break;
2044     }
2045     return 1;
2046 }
2047 
2048 /******************************************************************************
2049 * Called when an alarm action has completed, to perform any post-alarm actions.
2050 */
alarmCompleted(const KAEvent & event)2051 void KAlarmApp::alarmCompleted(const KAEvent& event)
2052 {
2053     if (!event.postAction().isEmpty())
2054     {
2055         // doShellCommand() will error if the user is not authorised to run
2056         // shell commands.
2057         const QString command = event.postAction();
2058         qCDebug(KALARM_LOG) << "KAlarmApp::alarmCompleted:" << event.id() << ":" << command;
2059         doShellCommand(command, event, nullptr, ProcData::POST_ACTION);
2060     }
2061 }
2062 
2063 /******************************************************************************
2064 * Reschedule the alarm for its next recurrence after now. If none remain,
2065 * delete it.  If the alarm is deleted and it is the last alarm for its event,
2066 * the event is removed from the calendar file and from every main window
2067 * instance.
2068 * If 'nextDt' is valid, the event is rescheduled for the next non-working
2069 * time occurrence after that.
2070 * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due
2071 *       = -1 if the event has been deleted
2072 *       = 0 otherwise.
2073 */
rescheduleAlarm(KAEvent & event,const KAAlarm & alarm,bool updateCalAndDisplay,const KADateTime & nextDt)2074 int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KADateTime& nextDt)
2075 {
2076     qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: Alarm type:" << alarm.type();
2077     int reply = 0;
2078     bool update = false;
2079     event.startChanges();
2080     if (alarm.repeatAtLogin())
2081     {
2082         // Leave an alarm which repeats at every login until its main alarm triggers
2083         if (!event.reminderActive()  &&  event.reminderMinutes() < 0)
2084         {
2085             // Executing an at-login alarm: first schedule the reminder
2086             // which occurs AFTER the main alarm.
2087             event.activateReminderAfter(KADateTime::currentUtcDateTime());
2088         }
2089         // Repeat-at-login alarms are usually unchanged after triggering.
2090         // Ensure that the archive flag (which was set in execAlarm()) is saved.
2091         update = true;
2092     }
2093     else if (alarm.isReminder()  ||  alarm.deferred())
2094     {
2095         // It's a reminder alarm or an extra deferred alarm, so delete it
2096         event.removeExpiredAlarm(alarm.type());
2097         update = true;
2098     }
2099     else
2100     {
2101         // Reschedule the alarm for its next occurrence.
2102         bool cancelled = false;
2103         DateTime last = event.mainDateTime(false);   // note this trigger time
2104         if (last != event.mainDateTime(true))
2105             last = DateTime();                       // but ignore sub-repetition triggers
2106         bool next = nextDt.isValid();
2107         KADateTime next_dt = nextDt;
2108         const KADateTime now = KADateTime::currentUtcDateTime();
2109         do
2110         {
2111             const KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now);
2112             switch (type)
2113             {
2114                 case KAEvent::NO_OCCURRENCE:
2115                     // All repetitions are finished, so cancel the event
2116                     qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: No occurrence";
2117                     if (event.reminderMinutes() < 0  &&  last.isValid()
2118                     &&  alarm.type() != KAAlarm::AT_LOGIN_ALARM  &&  !event.mainExpired())
2119                     {
2120                         // Set the reminder which is now due after the last main alarm trigger.
2121                         // Note that at-login reminders are scheduled in execAlarm().
2122                         event.activateReminderAfter(last);
2123                         updateCalAndDisplay = true;
2124                     }
2125                     if (cancelAlarm(event, alarm.type(), updateCalAndDisplay))
2126                         return -1;
2127                     break;
2128                 default:
2129                     if (!(type & KAEvent::OCCURRENCE_REPEAT))
2130                         break;
2131                     // Next occurrence is a repeat, so fall through to recurrence handling
2132                     Q_FALLTHROUGH();
2133                 case KAEvent::RECURRENCE_DATE:
2134                 case KAEvent::RECURRENCE_DATE_TIME:
2135                 case KAEvent::LAST_RECURRENCE:
2136                     // The event is due by now and repetitions still remain, so rewrite the event
2137                     if (updateCalAndDisplay)
2138                         update = true;
2139                     break;
2140                 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
2141                     // The first occurrence is still due?!?, so don't do anything
2142                     break;
2143             }
2144             if (cancelled)
2145                 break;
2146             if (event.deferred())
2147             {
2148                 // Just in case there's also a deferred alarm, ensure it's removed
2149                 event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM);
2150                 update = true;
2151             }
2152             if (next)
2153             {
2154                 // The alarm is restricted to working hours and/or non-holidays.
2155                 // Check if the calculated next time is valid.
2156                 next_dt = event.mainDateTime(true).effectiveKDateTime();
2157                 if (event.mainDateTime(false).isDateOnly())
2158                 {
2159                     KADateTime dt(next_dt);
2160                     dt.setDateOnly(true);
2161                     next = event.excludedByWorkTimeOrHoliday(dt);
2162                 }
2163                 else
2164                     next = event.excludedByWorkTimeOrHoliday(next_dt);
2165             }
2166         } while (next && next_dt <= now);
2167         reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0;
2168 
2169         if (event.reminderMinutes() < 0  &&  last.isValid()
2170         &&  alarm.type() != KAAlarm::AT_LOGIN_ALARM)
2171         {
2172             // Set the reminder which is now due after the last main alarm trigger.
2173             // Note that at-login reminders are scheduled in execAlarm().
2174             event.activateReminderAfter(last);
2175         }
2176     }
2177     event.endChanges();
2178     if (update)
2179         KAlarm::updateEvent(event, nullptr, true, false);   // update the window lists and calendar file
2180     return reply;
2181 }
2182 
2183 /******************************************************************************
2184 * Delete the alarm. If it is the last alarm for its event, the event is removed
2185 * from the calendar file and from every main window instance.
2186 * Reply = true if event has been deleted.
2187 */
cancelAlarm(KAEvent & event,KAAlarm::Type alarmType,bool updateCalAndDisplay)2188 bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay)
2189 {
2190     qCDebug(KALARM_LOG) << "KAlarmApp::cancelAlarm";
2191     if (alarmType == KAAlarm::MAIN_ALARM  &&  !event.displaying()  &&  event.toBeArchived())
2192     {
2193         // The event is being deleted. Save it in the archived resource first.
2194         Resource resource;
2195         KAEvent ev(event);
2196         KAlarm::addArchivedEvent(ev, resource);
2197     }
2198     event.removeExpiredAlarm(alarmType);
2199     if (!event.alarmCount())
2200     {
2201         // If it's a command alarm being executed, mark it as deleted
2202         ProcData* pd = findCommandProcess(event.id());
2203         if (pd)
2204             pd->eventDeleted = true;
2205 
2206         // Delete it
2207         Resource resource;
2208         KAlarm::deleteEvent(event, resource, false);
2209         return true;
2210     }
2211     if (updateCalAndDisplay)
2212         KAlarm::updateEvent(event);    // update the window lists and calendar file
2213     return false;
2214 }
2215 
2216 /******************************************************************************
2217 * Cancel any reminder or deferred alarms in an repeat-at-login event.
2218 * This should be called when the event is first loaded.
2219 * If there are no more alarms left in the event, the event is removed from the
2220 * calendar file and from every main window instance.
2221 * Reply = true if event has been deleted.
2222 */
cancelReminderAndDeferral(KAEvent & event)2223 bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event)
2224 {
2225     return cancelAlarm(event, KAAlarm::REMINDER_ALARM, false)
2226        ||  cancelAlarm(event, KAAlarm::DEFERRED_REMINDER_ALARM, false)
2227        ||  cancelAlarm(event, KAAlarm::DEFERRED_ALARM, true);
2228 }
2229 
2230 /******************************************************************************
2231 * Execute an alarm by displaying its message or file, or executing its command.
2232 * Reply = ShellProcess instance if a command alarm
2233 *       = MessageWindow if an audio alarm
2234 *       != null if successful
2235 *       = -1 if execution has not completed
2236 *       = -2 if can't execute display event because notifications are inhibited.
2237 *       = null if the alarm is disabled, or if an error message was output.
2238 */
execAlarm(KAEvent & event,const KAAlarm & alarm,ExecAlarmFlags flags)2239 void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, ExecAlarmFlags flags)
2240 {
2241     if (!mAlarmsEnabled  ||  !event.enabled())
2242     {
2243         // The event (or all events) is disabled
2244         qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": disabled";
2245         if (flags & Reschedule)
2246             rescheduleAlarm(event, alarm, true);
2247         return nullptr;
2248     }
2249 
2250     if (mNotificationsInhibited  &&  !(flags & NoNotifyInhibit)
2251     &&  (event.actionTypes() & KAEvent::ACT_DISPLAY))
2252     {
2253         // It's a display event and notifications are inhibited.
2254         qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": notifications inhibited";
2255         return (void*)-2;
2256     }
2257 
2258     void* result = (void*)1;
2259     event.setArchive();
2260 
2261     switch (alarm.action())
2262     {
2263         case KAAlarm::COMMAND:
2264             if (!event.commandDisplay())
2265             {
2266                 // execCommandAlarm() will error if the user is not authorised
2267                 // to run shell commands.
2268                 result = execCommandAlarm(event, alarm, flags & NoRecordCmdError);
2269                 if (flags & Reschedule)
2270                     rescheduleAlarm(event, alarm, true);
2271                 break;
2272             }
2273             Q_FALLTHROUGH();   // fall through to MESSAGE
2274         case KAAlarm::MESSAGE:
2275         case KAAlarm::FILE:
2276         {
2277             // Display a message, file or command output, provided that the same event
2278             // isn't already being displayed
2279             MessageDisplay* disp = MessageDisplay::findEvent(EventId(event));
2280             // Find if we're changing a reminder message to the real message
2281             const bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM);
2282             const bool replaceReminder = !reminder && disp && (disp->alarmType() & KAAlarm::REMINDER_ALARM);
2283             if (!reminder
2284             &&  (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral))
2285             &&  (replaceReminder || !disp)  &&  !(flags & NoPreAction)
2286             &&  !event.preAction().isEmpty())
2287             {
2288                 // It's not a reminder alarm, and it's not a deferred alarm unless the
2289                 // pre-alarm action applies to deferred alarms, and there is no message
2290                 // window (other than a reminder window) currently displayed for this
2291                 // alarm, and we need to execute a command before displaying the new window.
2292                 //
2293                 // NOTE: The pre-action is not executed for a recurring alarm if an
2294                 // alarm message window for a previous occurrence is still visible.
2295                 // Check whether the command is already being executed for this alarm.
2296                 for (const ProcData* pd : std::as_const(mCommandProcesses))
2297                 {
2298                     if (pd->event->id() == event.id()  &&  (pd->flags & ProcData::PRE_ACTION))
2299                     {
2300                         qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Already executing pre-DISPLAY command";
2301                         return pd->process;   // already executing - don't duplicate the action
2302                     }
2303                 }
2304 
2305                 // doShellCommand() will error if the user is not authorised to run
2306                 // shell commands.
2307                 const QString command = event.preAction();
2308                 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command;
2309                 const int pdFlags = (flags & Reschedule ? ProcData::RESCHEDULE : 0) | (flags & AllowDefer ? ProcData::ALLOW_DEFER : 0);
2310                 if (doShellCommand(command, event, &alarm, (pdFlags | ProcData::PRE_ACTION)))
2311                 {
2312                     ResourcesCalendar::setAlarmPending(event);
2313                     return result;     // display the message after the command completes
2314                 }
2315                 // Error executing command
2316                 if (event.extraActionOptions() & KAEvent::CancelOnPreActError)
2317                 {
2318                     // Cancel the rest of the alarm execution
2319                     qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": pre-action failed: cancelled";
2320                     if (flags & Reschedule)
2321                         rescheduleAlarm(event, alarm, true);
2322                     return nullptr;
2323                 }
2324                 // Display the message even though it failed
2325             }
2326 
2327             if (!disp)
2328             {
2329                 // There isn't already a message for this event
2330                 const int mdFlags = (flags & Reschedule ? 0 : MessageDisplay::NoReschedule)
2331                                   | (flags & AllowDefer ? 0 : MessageDisplay::NoDefer)
2332                                   | (flags & NoRecordCmdError ? MessageDisplay::NoRecordCmdError : 0);
2333                 MessageDisplay::create(event, alarm, mdFlags)->showDisplay();
2334             }
2335             else if (replaceReminder)
2336             {
2337                 // The caption needs to be changed from "Reminder" to "Message"
2338                 disp->cancelReminder(event, alarm);
2339             }
2340             else if (!disp->hasDefer() && event.repeatAtLogin() && !alarm.repeatAtLogin())
2341             {
2342                 // It's a repeat-at-login message with no Defer button,
2343                 // which has now reached its final trigger time and needs
2344                 // to be replaced with a new message.
2345                 disp->showDefer();
2346                 disp->showDateTime(event, alarm);
2347             }
2348             else
2349             {
2350                 // Use the existing message window
2351             }
2352             if (disp)
2353             {
2354                 // Raise the existing message window and replay any sound
2355                 disp->repeat(alarm);    // N.B. this reschedules the alarm
2356             }
2357             break;
2358         }
2359         case KAAlarm::EMAIL:
2360         {
2361             qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: EMAIL to:" << event.emailAddresses(QStringLiteral(","));
2362             QStringList errmsgs;
2363             KAMail::JobData data(event, alarm, flags & Reschedule, flags & (Reschedule | AllowDefer));
2364             data.queued = true;
2365             int ans = KAMail::send(data, errmsgs);
2366             if (ans)
2367             {
2368                 // The email has either been sent or failed - not queued
2369                 if (ans < 0)
2370                     result = nullptr;  // failure
2371                 data.queued = false;
2372                 emailSent(data, errmsgs, (ans > 0));
2373             }
2374             else
2375             {
2376                 result = (void*)-1;   // email has been queued
2377             }
2378             if (flags & Reschedule)
2379                 rescheduleAlarm(event, alarm, true);
2380             break;
2381         }
2382         case KAAlarm::AUDIO:
2383         {
2384             // Play the sound, provided that the same event
2385             // isn't already playing
2386             MessageDisplay* disp = MessageDisplay::findEvent(EventId(event));
2387             if (!disp)
2388             {
2389                 // There isn't already a message for this event.
2390                 const int mdFlags = (flags & Reschedule ? 0 : MessageDisplay::NoReschedule) | MessageDisplay::AlwaysHide;
2391                 event.setNotify(false);   // can't use notification system if audio only
2392                 disp = MessageDisplay::create(event, alarm, mdFlags);
2393             }
2394             else
2395             {
2396                 // There's an existing message window: replay the sound
2397                 disp->repeat(alarm);    // N.B. this reschedules the alarm
2398             }
2399             return dynamic_cast<MessageWindow*>(disp);
2400         }
2401         default:
2402             return nullptr;
2403     }
2404     return result;
2405 }
2406 
2407 /******************************************************************************
2408 * Called when sending an email has completed.
2409 */
emailSent(KAMail::JobData & data,const QStringList & errmsgs,bool copyerr)2410 void KAlarmApp::emailSent(KAMail::JobData& data, const QStringList& errmsgs, bool copyerr)
2411 {
2412     if (!errmsgs.isEmpty())
2413     {
2414         // Some error occurred, although the email may have been sent successfully
2415         if (errmsgs.count() > 1)
2416             qCDebug(KALARM_LOG) << "KAlarmApp::emailSent:" << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1];
2417         MessageDisplay::showError(data.event, data.alarm.dateTime(), errmsgs);
2418     }
2419     else if (data.queued)
2420         Q_EMIT execAlarmSuccess();
2421 }
2422 
2423 /******************************************************************************
2424 * Execute the command specified in a command alarm.
2425 * To connect to the output ready signals of the process, specify a slot to be
2426 * called by supplying 'receiver' and 'slot' parameters.
2427 */
execCommandAlarm(const KAEvent & event,const KAAlarm & alarm,bool noRecordError,QObject * receiver,const char * slotOutput,const char * methodExited)2428 ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, bool noRecordError,
2429                                           QObject* receiver, const char* slotOutput, const char* methodExited)
2430 {
2431     // doShellCommand() will error if the user is not authorised to run
2432     // shell commands.
2433     const int flags = (event.commandXterm()   ? ProcData::EXEC_IN_XTERM : 0)
2434                     | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0)
2435                     | (noRecordError          ? ProcData::NO_RECORD_ERROR : 0);
2436     const QString command = event.cleanText();
2437     if (event.commandScript())
2438     {
2439         // Store the command script in a temporary file for execution
2440         qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script";
2441         const QString tmpfile = createTempScriptFile(command, false, event, alarm);
2442         if (tmpfile.isEmpty())
2443         {
2444             setEventCommandError(event, KAEvent::CMD_ERROR);
2445             return nullptr;
2446         }
2447         return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slotOutput, methodExited);
2448     }
2449     else
2450     {
2451         qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm:" << command;
2452         return doShellCommand(command, event, &alarm, flags, receiver, slotOutput, methodExited);
2453     }
2454 }
2455 
2456 /******************************************************************************
2457 * Execute a shell command line specified by an alarm.
2458 * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via
2459 * execAlarm() once the command completes, the execAlarm() parameters being
2460 * derived from the remaining bits in 'flags'.
2461 * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is
2462 * a pre- or post-alarm action respectively.
2463 * To connect to the exited signal of the process, specify the name of a method
2464 * to be called by supplying 'receiver' and 'methodExited' parameters.
2465 * To connect to the output ready signals of the process, specify a slot to be
2466 * called by supplying 'receiver' and 'slotOutput' parameters.
2467 *
2468 * Note that if shell access is not authorised, the attempt to run the command
2469 * will be errored.
2470 *
2471 * Reply = process which has been started, or null if a process couldn't be started.
2472 */
doShellCommand(const QString & command,const KAEvent & event,const KAAlarm * alarm,int flags,QObject * receiver,const char * slotOutput,const char * methodExited)2473 ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QObject* receiver, const char* slotOutput, const char* methodExited)
2474 {
2475     qCDebug(KALARM_LOG) << "KAlarmApp::doShellCommand:" << command << "," << event.id();
2476     QIODevice::OpenMode mode = QIODevice::WriteOnly;
2477     QString cmd;
2478     QString tmpXtermFile;
2479     if (flags & ProcData::EXEC_IN_XTERM)
2480     {
2481         // Execute the command in a terminal window.
2482         cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile);
2483         if (cmd.isEmpty())
2484         {
2485             qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed (no terminal selected)";
2486             const QStringList errors{i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)")};
2487             commandErrorMsg(nullptr, event, alarm, flags, errors);
2488             return nullptr;
2489         }
2490     }
2491     else
2492     {
2493         cmd = command;
2494         mode = QIODevice::ReadWrite;
2495     }
2496 
2497     ProcData* pd = nullptr;
2498     ShellProcess* proc = nullptr;
2499     if (!cmd.isEmpty())
2500     {
2501         // Use ShellProcess, which automatically checks whether the user is
2502         // authorised to run shell commands.
2503         proc = new ShellProcess(cmd);
2504         proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true);
2505         proc->setOutputChannelMode(KProcess::MergedChannels);   // combine stdout & stderr
2506         connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited);
2507         if ((flags & ProcData::DISP_OUTPUT)  &&  receiver && slotOutput)
2508         {
2509             connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slotOutput);
2510             connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slotOutput);
2511         }
2512         if (mode == QIODevice::ReadWrite  &&  !event.logFile().isEmpty())
2513         {
2514             // Output is to be appended to a log file.
2515             // Set up a logging process to write the command's output to.
2516             QString heading;
2517             if (alarm  &&  alarm->dateTime().isValid())
2518             {
2519                 const QString dateTime = alarm->dateTime().formatLocale();
2520                 heading = QStringLiteral("\n******* KAlarm %1 *******\n").arg(dateTime);
2521             }
2522             else
2523                 heading = QStringLiteral("\n******* KAlarm *******\n");
2524             QFile logfile(event.logFile());
2525             if (logfile.open(QIODevice::Append | QIODevice::Text))
2526             {
2527                 QTextStream out(&logfile);
2528                 out << heading;
2529                 logfile.close();
2530             }
2531             proc->setStandardOutputFile(event.logFile(), QIODevice::Append);
2532         }
2533         pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : nullptr), flags);
2534         if (flags & ProcData::TEMP_FILE)
2535             pd->tempFiles += command;
2536         if (!tmpXtermFile.isEmpty())
2537             pd->tempFiles += tmpXtermFile;
2538         if (receiver && methodExited)
2539         {
2540             pd->exitReceiver = receiver;
2541             pd->exitMethod   = methodExited;
2542         }
2543         mCommandProcesses.append(pd);
2544         if (proc->start(mode))
2545             return proc;
2546     }
2547 
2548     // Error executing command - report it
2549     qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed to start";
2550     commandErrorMsg(proc, event, alarm, flags);
2551     if (pd)
2552     {
2553         mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd));
2554         delete pd;
2555     }
2556     return nullptr;
2557 }
2558 
2559 /******************************************************************************
2560 * Compose a command line to execute the given command in a terminal window.
2561 * 'tempScriptFile' receives the name of a temporary script file which is
2562 * invoked by the command line, if applicable.
2563 * Reply = command line, or empty string if error.
2564 */
composeXTermCommand(const QString & command,const KAEvent & event,const KAAlarm * alarm,int flags,QString & tempScriptFile) const2565 QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const
2566 {
2567     qCDebug(KALARM_LOG) << "KAlarmApp::composeXTermCommand:" << command << "," << event.id();
2568     tempScriptFile.clear();
2569     QString cmd = Preferences::cmdXTermCommand();
2570     if (cmd.isEmpty())
2571         return QString();   // no terminal application is configured
2572     cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName());  // set the terminal window title
2573     if (cmd.indexOf(QLatin1String("%C")) >= 0)
2574     {
2575         // Execute the command from a temporary script file
2576         if (flags & ProcData::TEMP_FILE)
2577             cmd.replace(QLatin1String("%C"), command);    // the command is already calling a temporary file
2578         else
2579         {
2580             tempScriptFile = createTempScriptFile(command, true, event, *alarm);
2581             if (tempScriptFile.isEmpty())
2582                 return QString();
2583             cmd.replace(QLatin1String("%C"), tempScriptFile);    // %C indicates where to insert the command
2584         }
2585     }
2586     else if (cmd.indexOf(QLatin1String("%W")) >= 0)
2587     {
2588         // Execute the command from a temporary script file,
2589         // with a sleep after the command is executed
2590         tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm);
2591         if (tempScriptFile.isEmpty())
2592             return QString();
2593         cmd.replace(QLatin1String("%W"), tempScriptFile);    // %w indicates where to insert the command
2594     }
2595     else if (cmd.indexOf(QLatin1String("%w")) >= 0)
2596     {
2597         // Append a sleep to the command.
2598         // Quote the command in case it contains characters such as [>|;].
2599         const QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400"));
2600         cmd.replace(QLatin1String("%w"), exec);    // %w indicates where to insert the command string
2601     }
2602     else
2603     {
2604         // Set the command to execute.
2605         // Put it in quotes in case it contains characters such as [>|;].
2606         const QString exec = KShell::quoteArg(command);
2607         if (cmd.indexOf(QLatin1String("%c")) >= 0)
2608             cmd.replace(QLatin1String("%c"), exec);    // %c indicates where to insert the command string
2609         else
2610             cmd.append(exec);           // otherwise, simply append the command string
2611     }
2612     return cmd;
2613 }
2614 
2615 /******************************************************************************
2616 * Create a temporary script file containing the specified command string.
2617 * Reply = path of temporary file, or null string if error.
2618 */
createTempScriptFile(const QString & command,bool insertShell,const KAEvent & event,const KAAlarm & alarm) const2619 QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const
2620 {
2621     QTemporaryFile tmpFile;
2622     tmpFile.setAutoRemove(false);     // don't delete file when it is destructed
2623     if (!tmpFile.open())
2624         qCCritical(KALARM_LOG) << "Unable to create a temporary script file";
2625     else
2626     {
2627         tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser);
2628         QTextStream stream(&tmpFile);
2629         if (insertShell)
2630             stream << "#!" << ShellProcess::shellPath() << "\n";
2631         stream << command;
2632         stream.flush();
2633         if (tmpFile.error() != QFile::NoError)
2634             qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file";
2635         else
2636             return tmpFile.fileName();
2637     }
2638 
2639     const QStringList errmsgs(i18nc("@info", "Error creating temporary script file"));
2640     MessageDisplay::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script"));
2641     return QString();
2642 }
2643 
2644 /******************************************************************************
2645 * Called when a command alarm's execution completes.
2646 */
slotCommandExited(ShellProcess * proc)2647 void KAlarmApp::slotCommandExited(ShellProcess* proc)
2648 {
2649     qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited";
2650     // Find this command in the command list
2651     for (int i = 0, end = mCommandProcesses.count();  i < end;  ++i)
2652     {
2653         ProcData* pd = mCommandProcesses.at(i);
2654         if (pd->process == proc)
2655         {
2656             // Found the command. Check its exit status.
2657             bool executeAlarm = pd->preAction();
2658             const ShellProcess::Status status = proc->status();
2659             if (status == ShellProcess::SUCCESS  &&  !proc->exitCode())
2660             {
2661                 qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": SUCCESS";
2662                 clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE
2663                                                  : pd->postAction() ? KAEvent::CMD_ERROR_POST
2664                                                  : KAEvent::CMD_ERROR);
2665             }
2666             else
2667             {
2668                 QString errmsg = proc->errorMessage();
2669                 if (status == ShellProcess::SUCCESS  ||  status == ShellProcess::NOT_FOUND)
2670                     qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode();
2671                 else
2672                     qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status;
2673                 if (pd->messageBoxParent)
2674                 {
2675                     // Close the existing informational KMessageBox for this process
2676                     const QList<QDialog*> dialogs = pd->messageBoxParent->findChildren<QDialog*>();
2677                     if (!dialogs.isEmpty())
2678                         delete dialogs[0];
2679                     setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE
2680                                                    : pd->postAction() ? KAEvent::CMD_ERROR_POST
2681                                                    : KAEvent::CMD_ERROR);
2682                     if (!pd->tempFile())
2683                     {
2684                         errmsg += QLatin1Char('\n');
2685                         errmsg += proc->command();
2686                     }
2687                     KAMessageBox::error(pd->messageBoxParent, errmsg);
2688                 }
2689                 else
2690                     commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags);
2691 
2692                 if (executeAlarm
2693                 &&  (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError))
2694                 {
2695                     qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": pre-action failed: cancelled";
2696                     if (pd->reschedule())
2697                         rescheduleAlarm(*pd->event, *pd->alarm, true);
2698                     executeAlarm = false;
2699                 }
2700             }
2701             if (pd->preAction())
2702                 ResourcesCalendar::setAlarmPending(*pd->event, false);
2703             if (executeAlarm)
2704             {
2705                 execAlarm(*pd->event, *pd->alarm,   (pd->reschedule()     ? Reschedule : NoExecFlag)
2706                                                   | (pd->allowDefer()     ? AllowDefer : NoExecFlag)
2707                                                   | (pd->noRecordCmdErr() ? NoRecordCmdError : NoExecFlag)
2708                                                   | NoPreAction);
2709             }
2710             mCommandProcesses.removeAt(i);
2711             if (pd->exitReceiver && !pd->exitMethod.isEmpty())
2712                 QMetaObject::invokeMethod(pd->exitReceiver, pd->exitMethod.constData(), Qt::DirectConnection, Q_ARG(ShellProcess::Status, status));
2713             delete pd;
2714             break;
2715         }
2716     }
2717 
2718     // If there are now no executing shell commands, quit if a quit was queued
2719     if (mPendingQuit  &&  mCommandProcesses.isEmpty())
2720         quitIf(mPendingQuitCode);
2721 }
2722 
2723 /******************************************************************************
2724 * Output an error message for a shell command, and record the alarm's error status.
2725 */
commandErrorMsg(const ShellProcess * proc,const KAEvent & event,const KAAlarm * alarm,int flags,const QStringList & errors)2726 void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors)
2727 {
2728     KAEvent::CmdErrType cmderr;
2729     QString dontShowAgain;
2730     QStringList errmsgs = errors;
2731     if (flags & ProcData::PRE_ACTION)
2732     {
2733         if (event.extraActionOptions() & KAEvent::DontShowPreActError)
2734             return;   // don't notify user of any errors for the alarm
2735         errmsgs += i18nc("@info", "Pre-alarm action:");
2736         dontShowAgain = QStringLiteral("Pre");
2737         cmderr = KAEvent::CMD_ERROR_PRE;
2738     }
2739     else if (flags & ProcData::POST_ACTION)
2740     {
2741         errmsgs += i18nc("@info", "Post-alarm action:");
2742         dontShowAgain = QStringLiteral("Post");
2743         cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE)
2744                ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST;
2745     }
2746     else
2747     {
2748         if (!event.commandHideError())
2749             dontShowAgain = QStringLiteral("Exec");
2750         cmderr = KAEvent::CMD_ERROR;
2751     }
2752 
2753     // Record the alarm's error status
2754     if (!(flags & ProcData::NO_RECORD_ERROR))
2755         setEventCommandError(event, cmderr);
2756 
2757     if (!dontShowAgain.isEmpty())
2758     {
2759         // Display an error message
2760         if (proc)
2761         {
2762             errmsgs += proc->errorMessage();
2763             if (!(flags & ProcData::TEMP_FILE))
2764                 errmsgs += proc->command();
2765             dontShowAgain += QString::number(proc->status());
2766         }
2767         MessageDisplay::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain);
2768     }
2769 }
2770 
2771 /******************************************************************************
2772 * Notes that an informational KMessageBox is displayed for this process.
2773 */
commandMessage(ShellProcess * proc,QWidget * parent)2774 void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent)
2775 {
2776     // Find this command in the command list
2777     for (ProcData* pd : std::as_const(mCommandProcesses))
2778     {
2779         if (pd->process == proc)
2780         {
2781             pd->messageBoxParent = parent;
2782             break;
2783         }
2784     }
2785 }
2786 
2787 /******************************************************************************
2788 * If this is the first time through, open the calendar file, and start
2789 * processing the execution queue.
2790 */
initCheck(bool calendarOnly)2791 bool KAlarmApp::initCheck(bool calendarOnly)
2792 {
2793     static bool firstTime = true;
2794     if (firstTime)
2795         qCDebug(KALARM_LOG) << "KAlarmApp::initCheck: first time";
2796 
2797     if (initialiseTimerResources()  ||  firstTime)
2798     {
2799         /* Need to open the display calendar now, since otherwise if display
2800          * alarms are immediately due, they will often be processed while
2801          * MessageDisplay::redisplayAlarms() is executing open() (but before
2802          * open() completes), which causes problems!!
2803          */
2804         DisplayCalendar::open();
2805     }
2806     if (firstTime)
2807     {
2808         setArchivePurgeDays();
2809 
2810         firstTime = false;
2811     }
2812 
2813     if (!calendarOnly)
2814         startProcessQueue();      // start processing the execution queue
2815 
2816     return true;
2817 }
2818 
2819 /******************************************************************************
2820 * Called when an audio thread starts or stops.
2821 */
notifyAudioPlaying(bool playing)2822 void KAlarmApp::notifyAudioPlaying(bool playing)
2823 {
2824     Q_EMIT audioPlaying(playing);
2825 }
2826 
2827 /******************************************************************************
2828 * Stop audio play.
2829 */
stopAudio()2830 void KAlarmApp::stopAudio()
2831 {
2832     MessageDisplay::stopAudio();
2833 }
2834 
2835 /******************************************************************************
2836 * Set the command error for the specified alarm.
2837 */
setEventCommandError(const KAEvent & event,KAEvent::CmdErrType err) const2838 void KAlarmApp::setEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const
2839 {
2840     ProcData* pd = findCommandProcess(event.id());
2841     if (pd && pd->eventDeleted)
2842         return;   // the alarm has been deleted, so can't set error status
2843 
2844     if (err == KAEvent::CMD_ERROR_POST  &&  event.commandError() == KAEvent::CMD_ERROR_PRE)
2845         err = KAEvent::CMD_ERROR_PRE_POST;
2846     event.setCommandError(err);
2847     KAEvent ev = ResourcesCalendar::event(EventId(event));
2848     if (ev.isValid()  &&  ev.commandError() != err)
2849     {
2850         ev.setCommandError(err);
2851         ResourcesCalendar::updateEvent(ev);
2852     }
2853     Resource resource = Resources::resourceForEvent(event.id());
2854     resource.handleCommandErrorChange(event);
2855 }
2856 
2857 /******************************************************************************
2858 * Clear the command error for the specified alarm.
2859 */
clearEventCommandError(const KAEvent & event,KAEvent::CmdErrType err) const2860 void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const
2861 {
2862     ProcData* pd = findCommandProcess(event.id());
2863     if (pd && pd->eventDeleted)
2864         return;   // the alarm has been deleted, so can't set error status
2865 
2866     auto newerr = static_cast<KAEvent::CmdErrType>(event.commandError() & ~err);
2867     event.setCommandError(newerr);
2868     KAEvent ev = ResourcesCalendar::event(EventId(event));
2869     if (ev.isValid())
2870     {
2871         newerr = static_cast<KAEvent::CmdErrType>(ev.commandError() & ~err);
2872         ev.setCommandError(newerr);
2873         ResourcesCalendar::updateEvent(ev);
2874     }
2875     Resource resource = Resources::resourceForEvent(event.id());
2876     resource.handleCommandErrorChange(event);
2877 }
2878 
2879 /******************************************************************************
2880 * Find the currently executing command process for an event ID, if any.
2881 */
findCommandProcess(const QString & eventId) const2882 KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const
2883 {
2884     for (ProcData* pd : std::as_const(mCommandProcesses))
2885     {
2886         if (pd->event->id() == eventId)
2887             return pd;
2888     }
2889     return nullptr;
2890 }
2891 
2892 
ProcData(ShellProcess * p,KAEvent * e,KAAlarm * a,int f)2893 KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f)
2894     : process(p)
2895     , event(e)
2896     , alarm(a)
2897     , flags(f)
2898 { }
2899 
~ProcData()2900 KAlarmApp::ProcData::~ProcData()
2901 {
2902     while (!tempFiles.isEmpty())
2903     {
2904         // Delete the temporary file called by the XTerm command
2905         QFile f(tempFiles.constFirst());
2906         f.remove();
2907         tempFiles.removeFirst();
2908     }
2909     delete process;
2910     delete event;
2911     delete alarm;
2912 }
2913 
2914 // vim: et sw=4:
2915