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

0001 /*
0002     CT Task 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 "cttask.h"
0010 
0011 #include <KLocalizedString>
0012 
0013 #include <QMimeDatabase>
0014 #include <QRegularExpression>
0015 #include <QUrl>
0016 
0017 #include "ctHelper.h"
0018 
0019 CTTask::CTTask(const QString &tokenString, const QString &_comment, const QString &_userLogin, bool _systemCrontab)
0020     : mSystemCrontab(_systemCrontab)
0021 {
0022     QString tokStr = tokenString;
0023     if (tokStr.mid(0, 2) == QLatin1String("#\\")) {
0024         tokStr = tokStr.mid(2, tokStr.length() - 2);
0025         enabled = false;
0026     } else if (tokStr.mid(0, 1) == QLatin1String("#")) {
0027         tokStr = tokStr.mid(1, tokStr.length() - 1);
0028         enabled = false;
0029     } else {
0030         enabled = true;
0031     }
0032 
0033     // Skip over 'silence' if found... old option in vixie cron
0034     if (tokStr.mid(0, 1) == QLatin1String("-")) {
0035         tokStr = tokStr.mid(1, tokStr.length() - 1);
0036     }
0037 
0038     reboot = false;
0039     if (tokStr.mid(0, 1) == QLatin1String("@")) {
0040         if (tokStr.mid(1, 6) == QLatin1String("yearly")) {
0041             tokStr = QLatin1String("0 0 1 1 *") + tokStr.mid(7, tokStr.length() - 1);
0042         } else if (tokStr.mid(1, 8) == QLatin1String("annually")) {
0043             tokStr = QLatin1String("0 0 1 1 *") + tokStr.mid(9, tokStr.length() - 1);
0044         } else if (tokStr.mid(1, 7) == QLatin1String("monthly")) {
0045             tokStr = QLatin1String("0 0 1 * *") + tokStr.mid(8, tokStr.length() - 1);
0046         } else if (tokStr.mid(1, 6) == QLatin1String("weekly")) {
0047             tokStr = QLatin1String("0 0 * * 0") + tokStr.mid(7, tokStr.length() - 1);
0048         } else if (tokStr.mid(1, 5) == QLatin1String("daily")) {
0049             tokStr = QLatin1String("0 0 * * *") + tokStr.mid(6, tokStr.length() - 1);
0050         } else if (tokStr.mid(1, 6) == QLatin1String("hourly")) {
0051             tokStr = QLatin1String("0 * * * *") + tokStr.mid(7, tokStr.length() - 1);
0052         } else if (tokStr.mid(1, 6) == QLatin1String("reboot")) {
0053             tokStr = tokStr.mid(7, tokStr.length() - 1);
0054             reboot = true;
0055         }
0056     }
0057 
0058     int spacePos(tokStr.indexOf(QRegularExpression(QLatin1String("[ \t]"))));
0059     // If reboot bypass initialize functions so no keys selected in modify task
0060     if (!reboot) {
0061         minute.initialize(tokStr.mid(0, spacePos));
0062 
0063         while (isSpaceAt(tokStr, spacePos + 1)) {
0064             spacePos++;
0065         }
0066         tokStr = tokStr.mid(spacePos + 1, tokStr.length() - 1);
0067         spacePos = tokStr.indexOf(QRegularExpression(QLatin1String("[ \t]")));
0068         hour.initialize(tokStr.mid(0, spacePos));
0069 
0070         while (isSpaceAt(tokStr, spacePos + 1)) {
0071             spacePos++;
0072         }
0073         tokStr = tokStr.mid(spacePos + 1, tokStr.length() - 1);
0074         spacePos = tokStr.indexOf(QRegularExpression(QLatin1String("[ \t]")));
0075         dayOfMonth.initialize(tokStr.mid(0, spacePos));
0076 
0077         while (isSpaceAt(tokStr, spacePos + 1)) {
0078             spacePos++;
0079         }
0080         tokStr = tokStr.mid(spacePos + 1, tokStr.length() - 1);
0081         spacePos = tokStr.indexOf(QRegularExpression(QLatin1String("[ \t]")));
0082         month.initialize(tokStr.mid(0, spacePos));
0083 
0084         while (isSpaceAt(tokStr, spacePos + 1)) {
0085             spacePos++;
0086         }
0087         tokStr = tokStr.mid(spacePos + 1, tokStr.length() - 1);
0088         spacePos = tokStr.indexOf(QRegularExpression(QLatin1String("[ \t]")));
0089         dayOfWeek.initialize(tokStr.mid(0, spacePos));
0090     }
0091 
0092     // Since it's a multiuser(system) task, the token contains the user login,
0093     // and the command, separated by a tab (\t).
0094     // The two need to subsequently be separated again.
0095     // E.g. "root\tmy_test_script.sh"
0096     if (mSystemCrontab) {
0097         while (isSpaceAt(tokStr, spacePos + 1)) {
0098             spacePos++;
0099         }
0100         tokStr = tokStr.mid(spacePos + 1, tokStr.length() - 1);
0101         spacePos = tokStr.indexOf(QRegularExpression(QLatin1String("[ \t]")));
0102         userLogin = tokStr.mid(0, spacePos);
0103     } else {
0104         userLogin = _userLogin;
0105     }
0106     command = tokStr.mid(spacePos + 1, tokStr.length() - 1);
0107     // remove leading whitespace
0108     while (command.indexOf(QRegularExpression(QLatin1String("[ \t]"))) == 0) {
0109         command = command.mid(1, command.length() - 1);
0110     }
0111     comment = _comment;
0112 
0113     mInitialUserLogin = userLogin;
0114     mInitialCommand = command;
0115     mInitialComment = comment;
0116     mInitialEnabled = enabled;
0117     mInitialReboot = reboot;
0118 }
0119 
0120 CTTask::CTTask(const CTTask &source)
0121     : month(source.month)
0122     , dayOfMonth(source.dayOfMonth)
0123     , dayOfWeek(source.dayOfWeek)
0124     , hour(source.hour)
0125     , minute(source.minute)
0126     , userLogin(source.userLogin)
0127     , command(source.command)
0128     , comment(source.comment)
0129     , enabled(source.enabled)
0130     , reboot(source.reboot)
0131     , mInitialUserLogin(QLatin1String(""))
0132     , mInitialCommand(QLatin1String(""))
0133     , mInitialComment(QLatin1String(""))
0134     , mInitialEnabled(true)
0135     , mInitialReboot(false)
0136 {
0137 }
0138 
0139 CTTask &CTTask::operator=(const CTTask &source)
0140 {
0141     if (this == &source) {
0142         return *this;
0143     }
0144 
0145     month = source.month;
0146     dayOfMonth = source.dayOfMonth;
0147     dayOfWeek = source.dayOfWeek;
0148     hour = source.hour;
0149     minute = source.minute;
0150     userLogin = source.userLogin;
0151     command = source.command;
0152     comment = source.comment;
0153     enabled = source.enabled;
0154     reboot = source.reboot;
0155     mInitialUserLogin = QLatin1String("");
0156     mInitialCommand = QLatin1String("");
0157     mInitialComment = QLatin1String("");
0158     mInitialEnabled = true;
0159     mInitialReboot = false;
0160 
0161     return *this;
0162 }
0163 
0164 QString CTTask::exportTask()
0165 {
0166     QString exportTask;
0167 
0168     exportTask += CTHelper::exportComment(comment);
0169 
0170     if (!enabled) {
0171         exportTask += QLatin1String("#\\");
0172     }
0173 
0174     exportTask += schedulingCronFormat();
0175     exportTask += QLatin1String("\t");
0176 
0177     if (isSystemCrontab()) {
0178         exportTask += userLogin + QLatin1String("\t");
0179     }
0180 
0181     exportTask += command + QLatin1String("\n");
0182 
0183     return exportTask;
0184 }
0185 
0186 void CTTask::apply()
0187 {
0188     month.apply();
0189     dayOfMonth.apply();
0190     dayOfWeek.apply();
0191     hour.apply();
0192     minute.apply();
0193 
0194     mInitialUserLogin = userLogin;
0195     mInitialCommand = command;
0196     mInitialComment = comment;
0197     mInitialEnabled = enabled;
0198     mInitialReboot = reboot;
0199 }
0200 
0201 void CTTask::cancel()
0202 {
0203     month.cancel();
0204     dayOfMonth.cancel();
0205     dayOfWeek.cancel();
0206     hour.cancel();
0207     minute.cancel();
0208 
0209     userLogin = mInitialUserLogin;
0210     command = mInitialCommand;
0211     comment = mInitialComment;
0212     enabled = mInitialEnabled;
0213     reboot = mInitialReboot;
0214 }
0215 
0216 bool CTTask::dirty() const
0217 {
0218     return month.isDirty() || dayOfMonth.isDirty() || dayOfWeek.isDirty() || hour.isDirty() || minute.isDirty() || (userLogin != mInitialUserLogin)
0219         || (command != mInitialCommand) || (comment != mInitialComment) || (enabled != mInitialEnabled) || (reboot != mInitialReboot);
0220 }
0221 
0222 QString CTTask::schedulingCronFormat() const
0223 {
0224     if (reboot) {
0225         return QStringLiteral("@reboot");
0226     }
0227 
0228     QString scheduling = minute.exportUnit() + QLatin1Char(' ');
0229     scheduling += hour.exportUnit() + QLatin1Char(' ');
0230     scheduling += dayOfMonth.exportUnit() + QLatin1Char(' ');
0231     scheduling += month.exportUnit() + QLatin1Char(' ');
0232     scheduling += dayOfWeek.exportUnit();
0233 
0234     return scheduling;
0235 }
0236 
0237 /**
0238  * Of the whole program, this method is probably the trickiest.
0239  *
0240  * This method creates the natural language description, such as
0241  * "At 1:00am, every Sun".
0242  *
0243  * First, I declare some strings for holding what can be internationalized.
0244  * Note the tokens such as "MONTHS".  Translators should reuse these
0245  * tokens in their translations.  See README.translators for more
0246  * information.
0247  *
0248  * Second, I get the descriptions from the component parts such as
0249  * days of the month.
0250  *
0251  * Third, I get hour/minute time combinations.  Although a little bit
0252  * awkward, I use the tm struct and strftime from <time.h>.  This
0253  * way this code is portable across all Unixes.
0254  *
0255  * Fourth, I know that "every day of the week" and "every day of the
0256  * month" simply makes "every day".
0257  *
0258  * Fifth and finally I do tag substitution to create the natural language
0259  * description.
0260  *
0261  */
0262 QString CTTask::describe() const
0263 {
0264     if (reboot) {
0265         return i18n("At system startup");
0266     }
0267 
0268     QString dateFormat = createDateFormat();
0269 
0270     QString timeFormat = createTimeFormat();
0271 
0272     return i18nc("1:Time Description, 2:Date Description", "%1, %2", timeFormat, dateFormat);
0273 }
0274 
0275 QString CTTask::describeDayOfWeek() const
0276 {
0277     return i18nc("Every 'days of week'", "every %1", dayOfWeek.describe());
0278 }
0279 
0280 QString CTTask::describeDayOfMonth() const
0281 {
0282     return i18nc("'Days of month' of 'Months'", "%1 of %2", dayOfMonth.describe(), month.describe());
0283 }
0284 
0285 QString CTTask::createDateFormat() const
0286 {
0287     /*
0288      * "* * *" means truly every day.
0289      * Note: Languages may use different phrases to indicate
0290      * every day of month versus every day of week.
0291      */
0292     QString dateFormat;
0293     if ((dayOfMonth.enabledCount() == CTDayOfMonth::MAXIMUM) && (dayOfWeek.enabledCount() == CTDayOfWeek::MAXIMUM)) {
0294         dateFormat = i18n("every day ");
0295     }
0296     // Day of month not specified, so use day of week.
0297     else if (dayOfMonth.enabledCount() == CTDayOfMonth::MAXIMUM) {
0298         dateFormat = describeDayOfWeek();
0299     }
0300     // Day of week not specified, so use day of month.
0301     else if (dayOfWeek.enabledCount() == CTDayOfWeek::MAXIMUM) {
0302         dateFormat = describeDayOfMonth();
0303     } else {
0304         dateFormat = i18nc("1:Day of month, 2:Day of week", "%1 as well as %2", describeDayOfMonth(), describeDayOfWeek());
0305     }
0306 
0307     return dateFormat;
0308 }
0309 
0310 QString CTTask::describeDateAndHours() const
0311 {
0312     // Create time description.
0313     int total = minute.enabledCount() * hour.enabledCount();
0314 
0315     QString timeDesc;
0316     int count = 0;
0317 
0318     for (int h = 0; h <= 23; h++) {
0319         if (hour.isEnabled(h)) {
0320             for (int m = 0; m <= 59; m++) {
0321                 if (minute.isEnabled(m)) {
0322                     QString hourString;
0323                     if (h < 10) {
0324                         hourString = QLatin1String("0") + QString::number(h);
0325                     } else {
0326                         hourString = QString::number(h);
0327                     }
0328 
0329                     QString minuteString;
0330                     if (m < 10) {
0331                         minuteString = QLatin1String("0") + QString::number(m);
0332                     } else {
0333                         minuteString = QString::number(m);
0334                     }
0335 
0336                     QString tmpStr = i18nc("1:Hour, 2:Minute", "%1:%2", hourString, minuteString);
0337 
0338                     timeDesc += tmpStr;
0339                     count++;
0340                     switch (total - count) {
0341                     case 0:
0342                         break;
0343                     case 1:
0344                         if (total > 2) {
0345                             timeDesc += i18n(", and ");
0346                         } else {
0347                             timeDesc += i18n(" and ");
0348                         }
0349                         break;
0350                     default:
0351                         timeDesc += i18n(", ");
0352                     }
0353                 }
0354             }
0355         }
0356     }
0357 
0358     return i18nc("Hour::Minute list", "At %1", timeDesc);
0359 }
0360 
0361 QString CTTask::createTimeFormat() const
0362 {
0363     if (hour.isAllEnabled()) {
0364         int minutePeriod = minute.findPeriod();
0365         if (minutePeriod != 0) {
0366             return i18np("Every minute", "Every %1 minutes", minutePeriod);
0367         }
0368     }
0369 
0370     return describeDateAndHours();
0371 }
0372 
0373 bool CTTask::isSystemCrontab() const
0374 {
0375     return mSystemCrontab;
0376 }
0377 
0378 void CTTask::setSystemCrontab(bool _systemCrontab)
0379 {
0380     mSystemCrontab = _systemCrontab;
0381 }
0382 
0383 QIcon CTTask::commandIcon() const
0384 {
0385     QUrl commandPath = QUrl::fromLocalFile(completeCommandPath());
0386 
0387     QMimeType mimeType = QMimeDatabase().mimeTypeForUrl(commandPath);
0388     // qCDebug(KCM_CRON_LOG) << mimeType->name();
0389     if (mimeType.name() == QLatin1String("application/x-executable") || mimeType.name() == QLatin1String("application/octet-stream")) {
0390         return QIcon::fromTheme(commandPath.fileName(), QIcon::fromTheme(QLatin1String("system-run")));
0391     }
0392 
0393     return QIcon::fromTheme(mimeType.iconName());
0394 }
0395 
0396 QPair<QString, bool> CTTask::unQuoteCommand() const
0397 {
0398     QString fullCommand = command;
0399     fullCommand = fullCommand.trimmed();
0400 
0401     const QStringList quotes{QStringLiteral("\""), QStringLiteral("'")};
0402 
0403     for (const QString &quote : quotes) {
0404         if (fullCommand.indexOf(quote) == 0) {
0405             int nextQuote = fullCommand.indexOf(quote, 1);
0406             if (nextQuote == -1) {
0407                 return QPair<QString, bool>(QLatin1String(""), false);
0408             }
0409 
0410             return QPair<QString, bool>(fullCommand.mid(1, nextQuote - 1), true);
0411         }
0412     }
0413 
0414     return QPair<QString, bool>(fullCommand, false);
0415 }
0416 
0417 QString CTTask::decryptBinaryCommand(const QString &command) const
0418 {
0419     QString fullCommand;
0420 
0421     bool found = false;
0422     for (int i = 0; i < command.length(); ++i) {
0423         if (command.at(i) == QLatin1Char(' ') && command.at(i - 1) != QLatin1Char('\\')) {
0424             fullCommand = command.left(i);
0425             found = true;
0426             break;
0427         }
0428     }
0429 
0430     if (!found) {
0431         fullCommand = command;
0432     }
0433 
0434     fullCommand.remove(QLatin1Char('\\'));
0435 
0436     return fullCommand;
0437 }
0438 
0439 QStringList CTTask::separatePathCommand(const QString &command, bool quoted) const
0440 {
0441     QStringList pathCommand;
0442 
0443     if (command.at(0) == QLatin1Char('/')) {
0444         QString fullCommand;
0445         if (quoted) {
0446             fullCommand = command;
0447         } else {
0448             fullCommand = decryptBinaryCommand(command);
0449         }
0450 
0451         if (fullCommand.isEmpty()) {
0452             return QStringList();
0453         }
0454 
0455         QString path = fullCommand.section(QLatin1Char('/'), 0, -2);
0456         QString commandBinary = fullCommand.section(QLatin1Char('/'), -1);
0457 
0458         pathCommand << path << commandBinary;
0459     } else {
0460         QString fullCommand;
0461         if (quoted) {
0462             fullCommand = command;
0463         } else {
0464             fullCommand = decryptBinaryCommand(command);
0465         }
0466 
0467         // relying on $PATH
0468         pathCommand << QString() << fullCommand;
0469     }
0470 
0471     return pathCommand;
0472 }
0473 
0474 QString CTTask::completeCommandPath() const
0475 {
0476     QPair<QString, bool> commandQuoted = unQuoteCommand();
0477     if (commandQuoted.first.isEmpty()) {
0478         return QLatin1String("");
0479     }
0480 
0481     QStringList pathCommand = separatePathCommand(commandQuoted.first, commandQuoted.second);
0482     if (pathCommand.isEmpty()) {
0483         return QLatin1String("");
0484     }
0485 
0486     return pathCommand.join(QLatin1String("/"));
0487 }