File indexing completed on 2024-04-28 05:36:11

0001 /*
0002  *  SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
0003  *  SPDX-FileCopyrightText: 2016 Marco Martin <mart@kde.org>
0004  *  SPDX-FileCopyrightText: 1998 Luca Montecchiani <m.luca@usa.net>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  *
0008  */
0009 
0010 #include "helper.h"
0011 #include "config-kcm.h"
0012 
0013 #include <algorithm>
0014 #include <chrono>
0015 
0016 #include <QDebug>
0017 #include <QDir>
0018 #include <QFile>
0019 #include <QMimeDatabase>
0020 #include <QProcess>
0021 #include <QRegularExpression>
0022 
0023 #include <KAuth/HelperSupport>
0024 
0025 #include "ktar.h"
0026 #include "kzip.h"
0027 #include <KArchive>
0028 #include <KConfigGroup>
0029 #include <KLocalizedString>
0030 #include <KSharedConfig>
0031 
0032 using namespace std::chrono_literals;
0033 
0034 static QString updateAlternatives()
0035 {
0036     return QStringLiteral("update-alternatives");
0037 }
0038 
0039 static bool hasUpdateAlternatives()
0040 {
0041     return !QStandardPaths::findExecutable(updateAlternatives()).isEmpty();
0042 }
0043 
0044 static ActionReply updateAlternativesInstall(const QString &installPath)
0045 {
0046     QProcess process;
0047     process.start(updateAlternatives(),
0048                   {QStringLiteral("--install"),
0049                    QStringLiteral("/usr/share/plymouth/themes/default.plymouth"),
0050                    QStringLiteral("default.plymouth"),
0051                    installPath,
0052                    QStringLiteral("100")});
0053 
0054     ActionReply reply = ActionReply::SuccessReply();
0055     if (!process.waitForStarted()) {
0056         reply = ActionReply::BackendError;
0057         reply.setErrorDescription(i18n("Cannot start update-alternatives."));
0058         return reply;
0059     }
0060     if (!process.waitForFinished()) {
0061         reply = ActionReply::BackendError;
0062         reply.setErrorDescription(i18n("update-alternatives failed to run."));
0063         return reply;
0064     }
0065 
0066     if (int ret = process.exitCode(); ret != 0) {
0067         reply = ActionReply(ActionReply::HelperErrorReply());
0068         reply.setErrorCode(static_cast<ActionReply::Error>(ret));
0069         reply.setErrorDescription(i18n("update-alternatives returned with error condition %1.", ret));
0070         return reply;
0071     }
0072     return reply;
0073 }
0074 
0075 ActionReply PlymouthHelper::save(const QVariantMap &args)
0076 {
0077     const QString theme = args.value(QStringLiteral("theme")).toString();
0078     ActionReply reply;
0079 
0080     if (theme.isEmpty()) {
0081         reply = ActionReply::BackendError;
0082         reply.setErrorDescription(i18n("No theme specified in helper parameters."));
0083         return reply;
0084     }
0085 
0086     {
0087         KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral(PLYMOUTH_CONFIG_PATH)), QStringLiteral("Daemon"));
0088         cg.writeEntry("Theme", theme);
0089     }
0090     QFile configFile(QStringLiteral(PLYMOUTH_CONFIG_PATH));
0091     configFile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther);
0092 
0093     // Special case: Ubuntu derivatives, which work different from everybody else
0094     if (hasUpdateAlternatives()) {
0095         // find the .plymouth file in the theme
0096         QDir dir(QStringLiteral(PLYMOUTH_THEMES_DIR) + theme);
0097         const QStringList themeFile = dir.entryList(QStringList() << QStringLiteral("*.plymouth"));
0098         if (themeFile.count() != 1) {
0099             reply = ActionReply::BackendError;
0100             reply.setErrorDescription(i18n("Theme corrupted: .plymouth file not found inside theme."));
0101             return reply;
0102         }
0103 
0104         QProcess checkProcess;
0105         QByteArray data;
0106         qDebug() << "Running update-alternatives --list default.plymouth now";
0107         checkProcess.start(updateAlternatives(), {QStringLiteral("--list"), QStringLiteral("default.plymouth")});
0108         if (!checkProcess.waitForStarted()) {
0109             reply = ActionReply::BackendError;
0110             reply.setErrorDescription(i18n("Cannot start update-alternatives."));
0111             return reply;
0112         }
0113         if (!checkProcess.waitForFinished()) {
0114             reply = ActionReply::BackendError;
0115             reply.setErrorDescription(i18n("update-alternatives failed to run."));
0116             return reply;
0117         }
0118         data = checkProcess.readAllStandardOutput();
0119 
0120         if (int ret = checkProcess.exitCode(); ret != 0) {
0121             reply = ActionReply(ActionReply::HelperErrorReply());
0122             reply.setErrorCode(static_cast<ActionReply::Error>(ret));
0123             reply.setErrorDescription(i18n("update-alternatives returned with error condition %1.", ret));
0124             return reply;
0125         }
0126         const QString installFile = dir.path() + QLatin1Char('/') + themeFile.first();
0127         if (!data.contains(installFile.toUtf8())) {
0128             qDebug() << "Plymouth file not found in update-alternatives. So install it";
0129             if (auto reply = updateAlternativesInstall(installFile); reply.failed()) {
0130                 return reply;
0131             }
0132         } else {
0133             qDebug() << "Running update-alternatives --set  now";
0134             QProcess process;
0135             process.start(QStringLiteral("update-alternatives"), QStringList() << QStringLiteral("--set") << QStringLiteral("default.plymouth") << installFile);
0136             if (!process.waitForStarted()) {
0137                 reply = ActionReply::BackendError;
0138                 reply.setErrorDescription(i18n("Cannot start update-alternatives."));
0139                 return reply;
0140             }
0141             if (!process.waitForFinished()) {
0142                 reply = ActionReply::BackendError;
0143                 reply.setErrorDescription(i18n("update-alternatives failed to run."));
0144                 return reply;
0145             }
0146 
0147             if (int ret = process.exitCode(); ret != 0) {
0148                 reply = ActionReply(ActionReply::HelperErrorReply());
0149                 reply.setErrorCode(static_cast<ActionReply::Error>(ret));
0150                 reply.setErrorDescription(i18n("update-alternatives returned with error condition %1.", ret));
0151                 return reply;
0152             }
0153         }
0154     }
0155 
0156     QProcess process;
0157     if (QFileInfo::exists(QStringLiteral("/usr/sbin/update-initramfs"))) {
0158         qDebug() << "Running update-initramfs -u now";
0159         process.start(QStringLiteral("/usr/sbin/update-initramfs"), QStringList() << QStringLiteral("-u"));
0160         if (!process.waitForStarted()) {
0161             reply = ActionReply::BackendError;
0162             reply.setErrorDescription(i18n("Cannot start initramfs."));
0163             return reply;
0164         }
0165     }
0166     if (QFileInfo::exists(QStringLiteral("/usr/bin/mkinitcpio"))) {
0167         qDebug() << "Running mkinitcpio -P now";
0168         process.start(QStringLiteral("/usr/bin/mkinitcpio"), QStringList() << QStringLiteral("-P"));
0169         if (!process.waitForStarted()) {
0170             reply = ActionReply::BackendError;
0171             reply.setErrorDescription(i18n("Cannot start mkinitcpio."));
0172             return reply;
0173         }
0174     }
0175     // We don't know how long this will take. The helper will need to generate N=installed_kernels initrds.
0176     // Be very generous with the timeout! https://bugs.kde.org/show_bug.cgi?id=400641
0177     // NB: there is also a timeout in the KCM
0178     if (!process.waitForFinished(std::chrono::milliseconds(15min).count())) {
0179         reply = ActionReply::BackendError;
0180         reply.setErrorDescription(i18n("Initramfs failed to run."));
0181         return reply;
0182     }
0183 
0184     int ret = process.exitCode();
0185     if (ret == 0) {
0186         return ActionReply::SuccessReply();
0187     }
0188     reply = ActionReply(ActionReply::HelperErrorReply());
0189     reply.setErrorCode(static_cast<ActionReply::Error>(ret));
0190     reply.setErrorDescription(i18n("Initramfs returned with error condition %1.", ret));
0191     return reply;
0192 }
0193 
0194 ActionReply PlymouthHelper::install(const QVariantMap &args)
0195 {
0196     const QString themearchive = args.value(QStringLiteral("themearchive")).toString();
0197     ActionReply reply;
0198 
0199     if (themearchive.isEmpty()) {
0200         return ActionReply::BackendError;
0201     }
0202 
0203     QDir basedir(QStringLiteral(PLYMOUTH_THEMES_DIR));
0204     if (!basedir.exists()) {
0205         return ActionReply::BackendError;
0206     }
0207 
0208     // this is weird but a decompression is not a single name, so take the path instead
0209     QString installpath = QStringLiteral(PLYMOUTH_THEMES_DIR);
0210     QMimeDatabase db;
0211     QMimeType mimeType = db.mimeTypeForFile(themearchive);
0212     qWarning() << "Postinstallation: uncompress the file";
0213 
0214     // FIXME: check for overwriting, malicious archive entries (../foo) etc.
0215     // FIXME: KArchive should provide "safe mode" for this!
0216     QScopedPointer<KArchive> archive;
0217 
0218     static QList<QString> tarTypes = {
0219         QStringLiteral("application/tar"),
0220         QStringLiteral("application/x-gzip"),
0221         QStringLiteral("application/x-bzip"),
0222         QStringLiteral("application/x-lzma"),
0223         QStringLiteral("application/x-xz"),
0224         QStringLiteral("application/x-bzip-compressed-tar"),
0225         QStringLiteral("application/x-compressed-tar"),
0226     };
0227 
0228     if (mimeType.inherits(QStringLiteral("application/zip"))) {
0229         archive.reset(new KZip(themearchive));
0230     } else if (std::any_of(tarTypes.cbegin(), tarTypes.cend(), [&](const auto &type) {
0231                    return mimeType.inherits(type);
0232                })) {
0233         archive.reset(new KTar(themearchive));
0234     } else {
0235         qCritical() << "Could not determine type of archive file '" << themearchive << "'";
0236         return ActionReply::BackendError;
0237     }
0238 
0239     if (!archive->open(QIODevice::ReadOnly)) {
0240         qCritical() << "Cannot open archive file '" << themearchive << "'";
0241         return ActionReply::BackendError;
0242     }
0243 
0244     QString themeName;
0245     QString themePath;
0246     const KArchiveDirectory *dir = archive->directory();
0247     // if there is more than an item in the file,
0248     // put contents in a subdirectory with the same name as the file
0249     if (dir->entries().count() > 1) {
0250         installpath += QLatin1Char('/') + QFileInfo(archive->fileName()).baseName();
0251         themeName = QFileInfo(archive->fileName()).baseName();
0252         themePath = installpath;
0253     } else {
0254         themeName = dir->entries().constFirst();
0255         themePath = installpath + dir->entries().constFirst();
0256     }
0257     dir->copyTo(installpath);
0258 
0259     archive->close();
0260 
0261     // Special case: Ubuntu derivatives, which work different from everybody else
0262     if (hasUpdateAlternatives()) {
0263         // find the .plymouth file in the theme
0264         QDir dir(themePath);
0265         const QStringList themeFile = dir.entryList({QStringLiteral("*.plymouth")});
0266         if (themeFile.count() != 1) {
0267             reply = ActionReply::BackendError;
0268             reply.setErrorDescription(i18n("Theme corrupted: .plymouth file not found inside theme."));
0269             return reply;
0270         }
0271 
0272         if (auto reply = updateAlternativesInstall(themePath + QLatin1Char('/') + themeFile.first()); reply.failed()) {
0273             return reply;
0274         }
0275     }
0276 
0277     QVariantMap map;
0278     map[QStringLiteral("plugin")] = themeName;
0279     map[QStringLiteral("path")] = themePath;
0280     reply = ActionReply::SuccessReply();
0281     reply.setData(map);
0282     return reply;
0283 }
0284 
0285 ActionReply PlymouthHelper::uninstall(const QVariantMap &args)
0286 {
0287     const QString theme = args.value(QStringLiteral("theme")).toString();
0288     ActionReply reply;
0289 
0290     if (theme.isEmpty()) {
0291         qWarning() << "No theme specified.";
0292         return ActionReply::BackendError;
0293     }
0294 
0295     QDir dir(QStringLiteral(PLYMOUTH_THEMES_DIR));
0296     if (!dir.exists()) {
0297         reply = ActionReply::BackendError;
0298         reply.setErrorDescription(i18n("Theme folder %1 does not exist.", QStringLiteral(PLYMOUTH_THEMES_DIR)));
0299         return reply;
0300     }
0301 
0302     if (!dir.cd(theme)) {
0303         reply = ActionReply::BackendError;
0304         reply.setErrorDescription(i18n("Theme %1 does not exist.", theme));
0305         return reply;
0306     }
0307 
0308     // Special case: Ubuntu derivatives, which work different from everybody else
0309     if (hasUpdateAlternatives()) {
0310         // find the .plymouth file in the theme
0311         const QStringList themeFile = dir.entryList(QStringList() << QStringLiteral("*.plymouth"));
0312         if (themeFile.count() != 1) {
0313             reply = ActionReply::BackendError;
0314             reply.setErrorDescription(i18n("Theme corrupted: .plymouth file not found inside theme."));
0315             return reply;
0316         }
0317         int ret = 0;
0318         QProcess process;
0319 
0320         process.start(QStringLiteral("update-alternatives"),
0321                       {QStringLiteral("--remove"), QStringLiteral("default.plymouth"), dir.path() + QLatin1Char('/') + themeFile.first()});
0322         if (!process.waitForStarted()) {
0323             reply = ActionReply::BackendError;
0324             reply.setErrorDescription(i18n("Cannot start update-alternatives."));
0325             return reply;
0326         }
0327         if (!process.waitForFinished()) {
0328             reply = ActionReply::BackendError;
0329             reply.setErrorDescription(i18n("update-alternatives failed to run."));
0330             return reply;
0331         }
0332         ret = process.exitCode();
0333 
0334         if (ret != 0) {
0335             reply = ActionReply(ActionReply::HelperErrorReply());
0336             reply.setErrorCode(static_cast<ActionReply::Error>(ret));
0337             reply.setErrorDescription(i18n("update-alternatives returned with error condition %1.", ret));
0338             return reply;
0339         }
0340     }
0341 
0342     if (dir.removeRecursively()) {
0343         return ActionReply::SuccessReply();
0344     }
0345     return ActionReply::BackendError;
0346 }
0347 
0348 KAUTH_HELPER_MAIN("org.kde.kcontrol.kcmplymouth", PlymouthHelper)
0349 
0350 #include "moc_helper.cpp"