File indexing completed on 2024-05-05 05:48:31

0001 /*
0002     CT Cron Implementation
0003     --------------------------------------------------------------------
0004     SPDX-FileCopyrightText: 1999 Gary Meyer <gary@meyer.net>
0005     --------------------------------------------------------------------
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "ctcron.h"
0010 
0011 #include <QDateTime>
0012 #include <QFile>
0013 #include <QProcess>
0014 #include <QRegularExpression>
0015 #include <QTemporaryFile>
0016 #include <QTextStream>
0017 
0018 #include <KLocalizedString>
0019 #include <KShell>
0020 
0021 #include "ctInitializationError.h"
0022 #include "cttask.h"
0023 #include "ctvariable.h"
0024 
0025 // For root permissions
0026 #include <KAuth/Action>
0027 #include <KAuth/ExecuteJob>
0028 #include <kauth_version.h>
0029 
0030 #include <pwd.h> // pwd, getpwnam(), getpwuid()
0031 #include <unistd.h> // getuid(), unlink()
0032 
0033 #include "kcm_cron_debug.h"
0034 
0035 CommandLineStatus CommandLine::execute()
0036 {
0037     QProcess process;
0038 
0039     int exitCode;
0040     process.start(commandLine, parameters);
0041     if (!process.waitForStarted()) {
0042         exitCode = 127;
0043     } else {
0044         process.waitForFinished(-1);
0045         exitCode = process.exitCode();
0046     }
0047 
0048     CommandLineStatus commandLineStatus;
0049 
0050     commandLineStatus.commandLine = commandLine + QLatin1String(" ") + parameters.join(QLatin1String(" "));
0051 
0052     commandLineStatus.standardOutput = QLatin1String(process.readAllStandardOutput());
0053     commandLineStatus.standardError = QLatin1String(process.readAllStandardError());
0054     commandLineStatus.exitCode = exitCode;
0055 
0056     return commandLineStatus;
0057 }
0058 
0059 CTCron::CTCron(const QString &crontabBinary, const struct passwd *userInfos, bool currentUserCron, CTInitializationError &ctInitializationError)
0060     : d(new CTCronPrivate())
0061 {
0062     Q_ASSERT(userInfos);
0063 
0064     d->multiUserCron = false;
0065     d->systemCron = false;
0066     d->currentUserCron = currentUserCron;
0067 
0068     d->crontabBinary = crontabBinary;
0069 
0070     CommandLine readCommandLine;
0071 
0072     // regular user, so provide user's own crontab
0073     if (currentUserCron) {
0074         readCommandLine.commandLine = d->crontabBinary;
0075         readCommandLine.parameters << QStringLiteral("-l");
0076     } else {
0077         readCommandLine.commandLine = d->crontabBinary;
0078         readCommandLine.parameters << QStringLiteral("-u") << QLatin1String(userInfos->pw_name) << QStringLiteral("-l");
0079     }
0080 
0081     d->initialTaskCount = 0;
0082     d->initialVariableCount = 0;
0083 
0084     if (!initializeFromUserInfos(userInfos)) {
0085         ctInitializationError.setErrorMessage(i18n("No password entry found for uid '%1'", getuid()));
0086         qCDebug(KCM_CRON_LOG) << "Error in crontab creation of" << userInfos->pw_name;
0087         return;
0088     }
0089 
0090     // Don't set error if it can't be read, it means the user doesn't have a crontab.
0091     CommandLineStatus commandLineStatus = readCommandLine.execute();
0092     if (commandLineStatus.exitCode == 0) {
0093         QTextStream stream(&commandLineStatus.standardOutput);
0094         parseTextStream(&stream);
0095     } else {
0096         qCDebug(KCM_CRON_LOG) << "Error when executing command" << commandLineStatus.commandLine;
0097         qCDebug(KCM_CRON_LOG) << "Standard output :" << commandLineStatus.standardOutput;
0098         qCDebug(KCM_CRON_LOG) << "Standard error :" << commandLineStatus.standardError;
0099     }
0100 
0101     d->initialTaskCount = d->task.size();
0102     d->initialVariableCount = d->variable.size();
0103 }
0104 
0105 CTCron::CTCron()
0106     : d(new CTCronPrivate())
0107 {
0108     // Empty constructor, new CTCronPrivate created.
0109 }
0110 
0111 bool CTCron::initializeFromUserInfos(const struct passwd *userInfos)
0112 {
0113     if (userInfos == nullptr) {
0114         return false;
0115     } else {
0116         d->userLogin = QLatin1String(userInfos->pw_name);
0117         d->userRealName = QLatin1String(userInfos->pw_gecos);
0118         return true;
0119     }
0120 }
0121 
0122 CTCron &CTCron::operator=(const CTCron &source)
0123 {
0124     if (this == &source) {
0125         return *this;
0126     }
0127 
0128     // Not sure when this would tigger.
0129     if (source.isSystemCron()) {
0130         qCDebug(KCM_CRON_LOG) << "Affect the system cron";
0131     }
0132 
0133     d->variable.clear();
0134     const auto variables = source.variables();
0135     for (CTVariable *ctVariable : variables) {
0136         auto tmp = new CTVariable(*ctVariable);
0137         d->variable.append(tmp);
0138     }
0139 
0140     d->task.clear();
0141     const auto tasks = source.tasks();
0142     for (CTTask *ctTask : tasks) {
0143         auto tmp = new CTTask(*ctTask);
0144         d->task.append(tmp);
0145     }
0146 
0147     return *this;
0148 }
0149 
0150 void CTCron::parseFile(const QString &fileName)
0151 {
0152     QFile file(fileName);
0153     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0154         return;
0155     }
0156 
0157     QTextStream in(&file);
0158     parseTextStream(&in);
0159 }
0160 
0161 void CTCron::parseTextStream(QTextStream *stream)
0162 {
0163     QString comment;
0164 
0165     while (!stream->atEnd()) {
0166         QString line = stream->readLine();
0167         // skip empty lines, empty lines are space between commented cron expressions
0168         if (line.isEmpty()) {
0169             comment.clear();
0170             continue;
0171         }
0172         // search for comments "#" but not disabled tasks "#\"
0173         // It's always loading comments user added by crontab -e command
0174         // or user added by Kcron
0175         if (line.startsWith(QLatin1Char('#')) && line.indexOf(QLatin1Char('\\')) != 1) {
0176             line = line.mid(1, line.length() - 1);
0177             if (comment.isEmpty()) {
0178                 comment = line.trimmed();
0179             } else {
0180                 comment += QLatin1Char('\n') + line.trimmed();
0181             }
0182             continue;
0183         }
0184 
0185         // either a task or a variable
0186         int firstWhiteSpace(line.indexOf(QRegularExpression(QLatin1String("[ \t]"))));
0187         int firstEquals(line.indexOf(QLatin1String("=")));
0188 
0189         // if there is an equals sign and either there is no
0190         // whitespace or the first whitespace is after the equals
0191         // sign, it must be a variable
0192         if ((firstEquals > 0) && ((firstWhiteSpace == -1) || firstWhiteSpace > firstEquals)) {
0193             // create variable
0194             auto tmp = new CTVariable(line, comment, d->userLogin);
0195             d->variable.append(tmp);
0196             comment.clear();
0197         }
0198         // must be a task, either enabled or disabled
0199         else {
0200             if (firstWhiteSpace > 0) {
0201                 auto tmp = new CTTask(line, comment, d->userLogin, d->multiUserCron);
0202                 d->task.append(tmp);
0203                 comment.clear();
0204             }
0205         }
0206     }
0207 }
0208 
0209 QString CTCron::exportCron() const
0210 {
0211     QString exportCron;
0212 
0213     for (CTVariable *ctVariable : std::as_const(d->variable)) {
0214         exportCron += ctVariable->exportVariable();
0215         exportCron += QLatin1String("\n");
0216     }
0217     for (CTTask *ctTask : std::as_const(d->task)) {
0218         exportCron += ctTask->exportTask();
0219         exportCron += QLatin1String("\n");
0220     }
0221 
0222     exportCron += QLatin1String("\n");
0223     QString exportInfo =
0224         i18nc("Generation Message + current date", "File generated by KCron the %1.", QDateTime::currentDateTime().toString(QLocale().dateTimeFormat()));
0225     exportCron += QLatin1String("# ") + exportInfo + QLatin1String("\n");
0226 
0227     return exportCron;
0228 }
0229 
0230 CTCron::~CTCron()
0231 {
0232     qDeleteAll(d->task);
0233 
0234     qDeleteAll(d->variable);
0235     delete d;
0236 }
0237 
0238 CTSaveStatus CTCron::prepareSaveStatusError(const CommandLineStatus &commandLineStatus)
0239 {
0240     QString standardOutput;
0241     if (commandLineStatus.standardOutput.isEmpty()) {
0242         standardOutput = i18n("<em>No output.</em>");
0243     } else {
0244         standardOutput = commandLineStatus.standardOutput;
0245     }
0246 
0247     QString standardError;
0248     if (commandLineStatus.standardError.isEmpty()) {
0249         standardError = i18n("<em>No error.</em>");
0250     } else {
0251         standardError = commandLineStatus.standardError;
0252     }
0253 
0254     QString detailError;
0255     if (commandLineStatus.exitCode == 127) {
0256         detailError = i18n("<p><strong>Command:</strong> %1</p><strong>Command could not be started</strong>", commandLineStatus.commandLine);
0257     } else {
0258         detailError = i18n("<p><strong>Command:</strong> %1</p><strong>Standard Output :</strong><pre>%2</pre><strong>Error Output :</strong><pre>%3</pre>",
0259                            commandLineStatus.commandLine,
0260                            standardOutput,
0261                            standardError);
0262     }
0263 
0264     return CTSaveStatus(i18n("An error occurred while updating crontab."), detailError);
0265 }
0266 
0267 CTSaveStatus CTCron::save()
0268 {
0269     // write to temp file
0270     QTemporaryFile tmp;
0271     if (!tmp.open()) {
0272         return CTSaveStatus(i18n("Unable to open crontab file for writing"), i18n("The file %1 could not be opened.", tmp.fileName()));
0273     }
0274 
0275     {
0276         QTextStream out(&tmp);
0277         out << exportCron();
0278         out.flush();
0279     }
0280     tmp.close();
0281 
0282     // For root permissions.
0283     if (d->systemCron) {
0284         qCDebug(KCM_CRON_LOG) << "Attempting to save system cron";
0285         QVariantMap args;
0286         args.insert(QStringLiteral("source"), tmp.fileName());
0287 
0288         KAuth::Action saveAction(QStringLiteral("local.kcron.crontab.save"));
0289         saveAction.setHelperId(QStringLiteral("local.kcron.crontab"));
0290         saveAction.setArguments(args);
0291         KAuth::ExecuteJob *job = saveAction.execute();
0292         if (!job->exec())
0293             qCDebug(KCM_CRON_LOG) << "KAuth returned an error: " << job->error() << job->errorText();
0294         if (job->error() > 0) {
0295             return CTSaveStatus(i18n("KAuth::ExecuteJob Error"), job->errorText());
0296         }
0297     }
0298     // End root permissions.
0299     else {
0300         qCDebug(KCM_CRON_LOG) << "Attempting to save user cron";
0301         // Save without root permissions.
0302         CommandLine writeCommandLine;
0303         writeCommandLine.commandLine = d->crontabBinary;
0304         if (d->currentUserCron) {
0305             writeCommandLine.parameters << tmp.fileName();
0306         } else {
0307             writeCommandLine.parameters << QStringLiteral("-u") << d->userLogin << tmp.fileName();
0308         }
0309 
0310         const CommandLineStatus commandLineStatus = writeCommandLine.execute();
0311         if (commandLineStatus.exitCode != 0) {
0312             return prepareSaveStatusError(commandLineStatus);
0313         }
0314     }
0315 
0316     // Mark as applied
0317     for (CTTask *ctTask : std::as_const(d->task)) {
0318         ctTask->apply();
0319     }
0320 
0321     for (CTVariable *ctVariable : std::as_const(d->variable)) {
0322         ctVariable->apply();
0323     }
0324 
0325     d->initialTaskCount = d->task.size();
0326     d->initialVariableCount = d->variable.size();
0327     qCDebug(KCM_CRON_LOG) << "All saved";
0328     return CTSaveStatus();
0329 }
0330 
0331 void CTCron::cancel()
0332 {
0333     const auto tasks = d->task;
0334     for (CTTask *ctTask : tasks) {
0335         ctTask->cancel();
0336     }
0337 
0338     const auto variables = d->variable;
0339     for (CTVariable *ctVariable : variables) {
0340         ctVariable->cancel();
0341     }
0342 }
0343 
0344 bool CTCron::isDirty() const
0345 {
0346     if (d->initialTaskCount != d->task.count()) {
0347         return true;
0348     }
0349 
0350     if (d->initialVariableCount != d->variable.count()) {
0351         return true;
0352     }
0353 
0354     for (CTTask *ctTask : std::as_const(d->task)) {
0355         if (ctTask->dirty()) {
0356             return true;
0357         }
0358     }
0359 
0360     for (CTVariable *ctVariable : std::as_const(d->variable)) {
0361         if (ctVariable->dirty()) {
0362             return true;
0363         }
0364     }
0365 
0366     return false;
0367 }
0368 
0369 QString CTCron::path() const
0370 {
0371     QString path;
0372 
0373     for (CTVariable *ctVariable : std::as_const(d->variable)) {
0374         if (ctVariable->variable == QLatin1String("PATH")) {
0375             path = ctVariable->value;
0376         }
0377     }
0378 
0379     return path;
0380 }
0381 
0382 QList<CTTask *> CTCron::tasks() const
0383 {
0384     return d->task;
0385 }
0386 
0387 QList<CTVariable *> CTCron::variables() const
0388 {
0389     return d->variable;
0390 }
0391 
0392 void CTCron::addTask(CTTask *task)
0393 {
0394     if (isSystemCron()) {
0395         task->setSystemCrontab(true);
0396     } else {
0397         task->userLogin = d->userLogin;
0398         task->setSystemCrontab(false);
0399     }
0400 
0401     qCDebug(KCM_CRON_LOG) << "Adding task" << task->comment << " user : " << task->userLogin;
0402 
0403     d->task.append(task);
0404 }
0405 
0406 void CTCron::addVariable(CTVariable *variable)
0407 {
0408     if (isSystemCron()) {
0409         variable->userLogin = QStringLiteral("root");
0410     } else {
0411         variable->userLogin = d->userLogin;
0412     }
0413 
0414     qCDebug(KCM_CRON_LOG) << "Adding variable" << variable->variable << " user : " << variable->userLogin;
0415 
0416     d->variable.append(variable);
0417 }
0418 
0419 void CTCron::modifyTask(CTTask * /*task*/)
0420 {
0421     // Nothing to do specifically when modifying a task
0422 }
0423 
0424 void CTCron::modifyVariable(CTVariable * /*variable*/)
0425 {
0426     // Nothing to do specifically when modifying a variable
0427 }
0428 
0429 void CTCron::removeTask(CTTask *task)
0430 {
0431     d->task.removeAll(task);
0432 }
0433 
0434 void CTCron::removeVariable(CTVariable *variable)
0435 {
0436     d->variable.removeAll(variable);
0437 }
0438 
0439 bool CTCron::isMultiUserCron() const
0440 {
0441     return d->multiUserCron;
0442 }
0443 
0444 bool CTCron::isCurrentUserCron() const
0445 {
0446     return d->currentUserCron;
0447 }
0448 
0449 bool CTCron::isSystemCron() const
0450 {
0451     return d->systemCron;
0452 }
0453 
0454 QString CTCron::userLogin() const
0455 {
0456     return d->userLogin;
0457 }
0458 
0459 QString CTCron::userRealName() const
0460 {
0461     return d->userRealName;
0462 }