File indexing completed on 2024-05-05 17:45:25

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)), "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     qDebug() << "Running update-initramfs -u  now";
0158     process.start(QStringLiteral("/usr/sbin/update-initramfs"), QStringList() << QStringLiteral("-u"));
0159     if (!process.waitForStarted()) {
0160         reply = ActionReply::BackendError;
0161         reply.setErrorDescription(i18n("Cannot start initramfs."));
0162         return reply;
0163     }
0164     // We don't know how long this will take. The helper will need to generate N=installed_kernels initrds.
0165     // Be very generous with the timeout! https://bugs.kde.org/show_bug.cgi?id=400641
0166     // NB: there is also a timeout in the KCM
0167     if (!process.waitForFinished(std::chrono::milliseconds(15min).count())) {
0168         reply = ActionReply::BackendError;
0169         reply.setErrorDescription(i18n("Initramfs failed to run."));
0170         return reply;
0171     }
0172 
0173     int ret = process.exitCode();
0174     if (ret == 0) {
0175         return ActionReply::SuccessReply();
0176     }
0177     reply = ActionReply(ActionReply::HelperErrorReply());
0178     reply.setErrorCode(static_cast<ActionReply::Error>(ret));
0179     reply.setErrorDescription(i18n("Initramfs returned with error condition %1.", ret));
0180     return reply;
0181 }
0182 
0183 ActionReply PlymouthHelper::install(const QVariantMap &args)
0184 {
0185     const QString themearchive = args.value(QStringLiteral("themearchive")).toString();
0186     ActionReply reply;
0187 
0188     if (themearchive.isEmpty()) {
0189         return ActionReply::BackendError;
0190     }
0191 
0192     QDir basedir(QStringLiteral(PLYMOUTH_THEMES_DIR));
0193     if (!basedir.exists()) {
0194         return ActionReply::BackendError;
0195     }
0196 
0197     // this is weird but a decompression is not a single name, so take the path instead
0198     QString installpath = QStringLiteral(PLYMOUTH_THEMES_DIR);
0199     QMimeDatabase db;
0200     QMimeType mimeType = db.mimeTypeForFile(themearchive);
0201     qWarning() << "Postinstallation: uncompress the file";
0202 
0203     // FIXME: check for overwriting, malicious archive entries (../foo) etc.
0204     // FIXME: KArchive should provide "safe mode" for this!
0205     QScopedPointer<KArchive> archive;
0206 
0207     static QVector<QString> tarTypes = {
0208         QStringLiteral("application/tar"),
0209         QStringLiteral("application/x-gzip"),
0210         QStringLiteral("application/x-bzip"),
0211         QStringLiteral("application/x-lzma"),
0212         QStringLiteral("application/x-xz"),
0213         QStringLiteral("application/x-bzip-compressed-tar"),
0214         QStringLiteral("application/x-compressed-tar"),
0215     };
0216 
0217     if (mimeType.inherits(QStringLiteral("application/zip"))) {
0218         archive.reset(new KZip(themearchive));
0219     } else if (std::any_of(tarTypes.cbegin(), tarTypes.cend(), [&](const auto &type) {
0220                    return mimeType.inherits(type);
0221                })) {
0222         archive.reset(new KTar(themearchive));
0223     } else {
0224         qCritical() << "Could not determine type of archive file '" << themearchive << "'";
0225         return ActionReply::BackendError;
0226     }
0227 
0228     if (!archive->open(QIODevice::ReadOnly)) {
0229         qCritical() << "Cannot open archive file '" << themearchive << "'";
0230         return ActionReply::BackendError;
0231     }
0232 
0233     QString themeName;
0234     QString themePath;
0235     const KArchiveDirectory *dir = archive->directory();
0236     // if there is more than an item in the file,
0237     // put contents in a subdirectory with the same name as the file
0238     if (dir->entries().count() > 1) {
0239         installpath += QLatin1Char('/') + QFileInfo(archive->fileName()).baseName();
0240         themeName = QFileInfo(archive->fileName()).baseName();
0241         themePath = installpath;
0242     } else {
0243         themeName = dir->entries().constFirst();
0244         themePath = installpath + dir->entries().constFirst();
0245     }
0246     dir->copyTo(installpath);
0247 
0248     const QStringList themeFileList = dir->entries().filter(QRegularExpression(QStringLiteral("\\.plymouth$")));
0249 
0250     archive->close();
0251 
0252     // Special case: Ubuntu derivatives, which work different from everybody else
0253     if (hasUpdateAlternatives()) {
0254         // find the .plymouth file in the theme
0255         QDir dir(themePath);
0256         const QStringList themeFile = dir.entryList({QStringLiteral("*.plymouth")});
0257         if (themeFile.count() != 1) {
0258             reply = ActionReply::BackendError;
0259             reply.setErrorDescription(i18n("Theme corrupted: .plymouth file not found inside theme."));
0260             return reply;
0261         }
0262 
0263         if (auto reply = updateAlternativesInstall(themePath + QLatin1Char('/') + themeFile.first()); reply.failed()) {
0264             return reply;
0265         }
0266     }
0267 
0268     QVariantMap map;
0269     map[QStringLiteral("plugin")] = themeName;
0270     map[QStringLiteral("path")] = themePath;
0271     reply = ActionReply::SuccessReply();
0272     reply.setData(map);
0273     return reply;
0274 }
0275 
0276 ActionReply PlymouthHelper::uninstall(const QVariantMap &args)
0277 {
0278     const QString theme = args.value(QStringLiteral("theme")).toString();
0279     ActionReply reply;
0280 
0281     if (theme.isEmpty()) {
0282         qWarning() << "No theme specified.";
0283         return ActionReply::BackendError;
0284     }
0285 
0286     QDir dir(QStringLiteral(PLYMOUTH_THEMES_DIR));
0287     if (!dir.exists()) {
0288         reply = ActionReply::BackendError;
0289         reply.setErrorDescription(i18n("Theme folder %1 does not exist.", QStringLiteral(PLYMOUTH_THEMES_DIR)));
0290         return reply;
0291     }
0292 
0293     if (!dir.cd(theme)) {
0294         reply = ActionReply::BackendError;
0295         reply.setErrorDescription(i18n("Theme %1 does not exist.", theme));
0296         return reply;
0297     }
0298 
0299     // Special case: Ubuntu derivatives, which work different from everybody else
0300     if (hasUpdateAlternatives()) {
0301         // find the .plymouth file in the theme
0302         const QStringList themeFile = dir.entryList(QStringList() << QStringLiteral("*.plymouth"));
0303         if (themeFile.count() != 1) {
0304             reply = ActionReply::BackendError;
0305             reply.setErrorDescription(i18n("Theme corrupted: .plymouth file not found inside theme."));
0306             return reply;
0307         }
0308         int ret = 0;
0309         QProcess process;
0310 
0311         process.start(QStringLiteral("update-alternatives"),
0312                       {QStringLiteral("--remove"), QStringLiteral("default.plymouth"), dir.path() + QLatin1Char('/') + themeFile.first()});
0313         if (!process.waitForStarted()) {
0314             reply = ActionReply::BackendError;
0315             reply.setErrorDescription(i18n("Cannot start update-alternatives."));
0316             return reply;
0317         }
0318         if (!process.waitForFinished()) {
0319             reply = ActionReply::BackendError;
0320             reply.setErrorDescription(i18n("update-alternatives failed to run."));
0321             return reply;
0322         }
0323         ret = process.exitCode();
0324 
0325         if (ret != 0) {
0326             reply = ActionReply(ActionReply::HelperErrorReply());
0327             reply.setErrorCode(static_cast<ActionReply::Error>(ret));
0328             reply.setErrorDescription(i18n("update-alternatives returned with error condition %1.", ret));
0329             return reply;
0330         }
0331     }
0332 
0333     if (dir.removeRecursively()) {
0334         return ActionReply::SuccessReply();
0335     }
0336     return ActionReply::BackendError;
0337 }
0338 
0339 KAUTH_HELPER_MAIN("org.kde.kcontrol.kcmplymouth", PlymouthHelper)