File indexing completed on 2024-05-19 16:49:24

0001 /*
0002  * SPDX-FileCopyrightText: 2019 Alexander Potashev <aspotashev@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include <KLocalizedString>
0008 #include <KShell>
0009 #include <QCommandLineParser>
0010 #include <QDebug>
0011 #include <QDir>
0012 #include <QDirIterator>
0013 #include <QGuiApplication>
0014 #include <QMimeDatabase>
0015 #include <QProcess>
0016 #include <QStandardPaths>
0017 #include <QTimer>
0018 #include <QUrl>
0019 
0020 #include "../../../config-dolphin.h"
0021 
0022 Q_GLOBAL_STATIC_WITH_ARGS(QStringList,
0023                           binaryPackages,
0024                           ({QLatin1String("application/vnd.debian.binary-package"),
0025                             QLatin1String("application/x-rpm"),
0026                             QLatin1String("application/x-xz"),
0027                             QLatin1String("application/zstd")}))
0028 
0029 enum PackageOperation { Install, Uninstall };
0030 
0031 #if HAVE_PACKAGEKIT
0032 #include <PackageKit/Daemon>
0033 #include <PackageKit/Details>
0034 #include <PackageKit/Transaction>
0035 #else
0036 #include <QDesktopServices>
0037 #endif
0038 
0039 // @param msg Error that gets logged to CLI
0040 Q_NORETURN void fail(const QString &str)
0041 {
0042     qCritical() << str;
0043     const QStringList args = {"--detailederror", i18n("Dolphin service menu installation failed"), str};
0044     QProcess::startDetached("kdialog", args);
0045 
0046     exit(1);
0047 }
0048 
0049 QString getServiceMenusDir()
0050 {
0051     const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
0052     return QDir(dataLocation).absoluteFilePath("kio/servicemenus");
0053 }
0054 
0055 #if HAVE_PACKAGEKIT
0056 void packageKitInstall(const QString &fileName)
0057 {
0058     PackageKit::Transaction *transaction = PackageKit::Daemon::installFile(fileName, PackageKit::Transaction::TransactionFlagNone);
0059 
0060     const auto exitWithError = [=](PackageKit::Transaction::Error, const QString &details) {
0061         fail(details);
0062     };
0063 
0064     QObject::connect(transaction, &PackageKit::Transaction::finished, [=](PackageKit::Transaction::Exit status, uint) {
0065         if (status == PackageKit::Transaction::ExitSuccess) {
0066             exit(0);
0067         }
0068         // Fallback error handling
0069         QTimer::singleShot(500, [=]() {
0070             fail(i18n("Failed to install \"%1\", exited with status \"%2\"", fileName, QVariant::fromValue(status).toString()));
0071         });
0072     });
0073     QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
0074 }
0075 
0076 void packageKitUninstall(const QString &fileName)
0077 {
0078     const auto exitWithError = [=](PackageKit::Transaction::Error, const QString &details) {
0079         fail(details);
0080     };
0081     const auto uninstallLambda = [=](PackageKit::Transaction::Exit status, uint) {
0082         if (status == PackageKit::Transaction::ExitSuccess) {
0083             exit(0);
0084         }
0085     };
0086 
0087     PackageKit::Transaction *transaction = PackageKit::Daemon::getDetailsLocal(fileName);
0088     QObject::connect(transaction, &PackageKit::Transaction::details, [=](const PackageKit::Details &details) {
0089         PackageKit::Transaction *transaction = PackageKit::Daemon::removePackage(details.packageId());
0090         QObject::connect(transaction, &PackageKit::Transaction::finished, uninstallLambda);
0091         QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
0092     });
0093 
0094     QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
0095     // Fallback error handling
0096     QObject::connect(transaction, &PackageKit::Transaction::finished, [=](PackageKit::Transaction::Exit status, uint) {
0097         if (status != PackageKit::Transaction::ExitSuccess) {
0098             QTimer::singleShot(500, [=]() {
0099                 fail(i18n("Failed to uninstall \"%1\", exited with status \"%2\"", fileName, QVariant::fromValue(status).toString()));
0100             });
0101         }
0102     });
0103 }
0104 #endif
0105 
0106 Q_NORETURN void packageKit(PackageOperation operation, const QString &fileName)
0107 {
0108 #if HAVE_PACKAGEKIT
0109     QFileInfo fileInfo(fileName);
0110     if (!fileInfo.exists()) {
0111         fail(i18n("The file does not exist!"));
0112     }
0113     const QString absPath = fileInfo.absoluteFilePath();
0114     if (operation == PackageOperation::Install) {
0115         packageKitInstall(absPath);
0116     } else {
0117         packageKitUninstall(absPath);
0118     }
0119     QGuiApplication::exec(); // For event handling, no return after signals finish
0120     fail(i18n("Unknown error when installing package"));
0121 #else
0122     Q_UNUSED(operation)
0123     QDesktopServices::openUrl(QUrl(fileName));
0124     exit(0);
0125 #endif
0126 }
0127 
0128 struct UncompressCommand {
0129     QString command;
0130     QStringList args1;
0131     QStringList args2;
0132 };
0133 
0134 enum ScriptExecution { Process, Konsole };
0135 
0136 void runUncompress(const QString &inputPath, const QString &outputPath)
0137 {
0138     QVector<QPair<QStringList, UncompressCommand>> mimeTypeToCommand;
0139     mimeTypeToCommand.append({{"application/x-tar", "application/tar", "application/x-gtar", "multipart/x-tar"}, UncompressCommand({"tar", {"-xf"}, {"-C"}})});
0140     mimeTypeToCommand.append({{"application/x-gzip",
0141                                "application/gzip",
0142                                "application/x-gzip-compressed-tar",
0143                                "application/gzip-compressed-tar",
0144                                "application/x-gzip-compressed",
0145                                "application/gzip-compressed",
0146                                "application/tgz",
0147                                "application/x-compressed-tar",
0148                                "application/x-compressed-gtar",
0149                                "file/tgz",
0150                                "multipart/x-tar-gz",
0151                                "application/x-gunzip",
0152                                "application/gzipped",
0153                                "gzip/document"},
0154                               UncompressCommand({"tar", {"-zxf"}, {"-C"}})});
0155     mimeTypeToCommand.append({{"application/bzip",
0156                                "application/bzip2",
0157                                "application/x-bzip",
0158                                "application/x-bzip2",
0159                                "application/bzip-compressed",
0160                                "application/bzip2-compressed",
0161                                "application/x-bzip-compressed",
0162                                "application/x-bzip2-compressed",
0163                                "application/bzip-compressed-tar",
0164                                "application/bzip2-compressed-tar",
0165                                "application/x-bzip-compressed-tar",
0166                                "application/x-bzip2-compressed-tar",
0167                                "application/x-bz2"},
0168                               UncompressCommand({"tar", {"-jxf"}, {"-C"}})});
0169     mimeTypeToCommand.append(
0170         {{"application/zip", "application/x-zip", "application/x-zip-compressed", "multipart/x-zip"}, UncompressCommand({"unzip", {}, {"-d"}})});
0171 
0172     const auto mime = QMimeDatabase().mimeTypeForFile(inputPath).name();
0173 
0174     UncompressCommand command{};
0175     for (const auto &pair : std::as_const(mimeTypeToCommand)) {
0176         if (pair.first.contains(mime)) {
0177             command = pair.second;
0178             break;
0179         }
0180     }
0181 
0182     if (command.command.isEmpty()) {
0183         fail(i18n("Unsupported archive type %1: %2", mime, inputPath));
0184     }
0185 
0186     QProcess process;
0187     process.start(command.command, QStringList() << command.args1 << inputPath << command.args2 << outputPath, QIODevice::NotOpen);
0188     if (!process.waitForStarted()) {
0189         fail(i18n("Failed to run uncompressor command for %1", inputPath));
0190     }
0191 
0192     if (!process.waitForFinished()) {
0193         fail(i18n("Process did not finish in reasonable time: %1 %2", process.program(), process.arguments().join(" ")));
0194     }
0195 
0196     if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
0197         fail(i18n("Failed to uncompress %1", inputPath));
0198     }
0199 }
0200 
0201 QString findRecursive(const QString &dir, const QString &basename)
0202 {
0203     QDirIterator it(dir, QStringList{basename}, QDir::Files, QDirIterator::Subdirectories);
0204     while (it.hasNext()) {
0205         return QFileInfo(it.next()).absoluteFilePath();
0206     }
0207 
0208     return QString();
0209 }
0210 
0211 bool runScriptOnce(const QString &path, const QStringList &args, ScriptExecution execution)
0212 {
0213     QProcess process;
0214     process.setWorkingDirectory(QFileInfo(path).absolutePath());
0215 
0216     const static bool konsoleAvailable = !QStandardPaths::findExecutable("konsole").isEmpty();
0217     if (konsoleAvailable && execution == ScriptExecution::Konsole) {
0218         QString bashCommand = KShell::quoteArg(path) + ' ';
0219         if (!args.isEmpty()) {
0220             bashCommand.append(args.join(' '));
0221         }
0222         bashCommand.append("|| $SHELL");
0223         // If the install script fails a shell opens and the user can fix the problem
0224         // without an error konsole closes
0225         process.start("konsole",
0226                       QStringList() << "-e"
0227                                     << "bash"
0228                                     << "-c" << bashCommand,
0229                       QIODevice::NotOpen);
0230     } else {
0231         process.start(path, args, QIODevice::NotOpen);
0232     }
0233     if (!process.waitForStarted()) {
0234         fail(i18n("Failed to run installer script %1", path));
0235     }
0236 
0237     // Wait until installer exits, without timeout
0238     if (!process.waitForFinished(-1)) {
0239         qWarning() << "Failed to wait on installer:" << process.program() << process.arguments().join(" ");
0240         return false;
0241     }
0242 
0243     if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
0244         qWarning() << "Installer script exited with error:" << process.program() << process.arguments().join(" ");
0245         return false;
0246     }
0247 
0248     return true;
0249 }
0250 
0251 // If hasArgVariants is true, run "path".
0252 // If hasArgVariants is false, run "path argVariants[i]" until successful.
0253 bool runScriptVariants(const QString &path, bool hasArgVariants, const QStringList &argVariants, QString &errorText)
0254 {
0255     QFile file(path);
0256     if (!file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) {
0257         errorText = i18n("Failed to set permissions on %1: %2", path, file.errorString());
0258         return false;
0259     }
0260 
0261     qInfo() << "[servicemenuinstaller]: Trying to run installer/uninstaller" << path;
0262     if (hasArgVariants) {
0263         for (const auto &arg : argVariants) {
0264             if (runScriptOnce(path, {arg}, ScriptExecution::Process)) {
0265                 return true;
0266             }
0267         }
0268     } else if (runScriptOnce(path, {}, ScriptExecution::Konsole)) {
0269         return true;
0270     }
0271 
0272     errorText = i18nc("%2 = comma separated list of arguments",
0273                       "Installer script %1 failed, tried arguments \"%2\".",
0274                       path,
0275                       argVariants.join(i18nc("Separator between arguments", "\", \"")));
0276     return false;
0277 }
0278 
0279 QString generateDirPath(const QString &archive)
0280 {
0281     return QStringLiteral("%1-dir").arg(archive);
0282 }
0283 
0284 bool cmdInstall(const QString &archive, QString &errorText)
0285 {
0286     const auto serviceDir = getServiceMenusDir();
0287     if (!QDir().mkpath(serviceDir)) {
0288         // TODO Cannot get error string because of this bug: https://bugreports.qt.io/browse/QTBUG-1483
0289         errorText = i18n("Failed to create path %1", serviceDir);
0290         return false;
0291     }
0292 
0293     if (archive.endsWith(QLatin1String(".desktop"))) {
0294         // Append basename to destination directory
0295         const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
0296         if (QFileInfo::exists(dest)) {
0297             QFile::remove(dest);
0298         }
0299         qInfo() << "Single-File Service-Menu" << archive << dest;
0300 
0301         QFile source(archive);
0302         if (!source.copy(dest)) {
0303             errorText = i18n("Failed to copy .desktop file %1 to %2: %3", archive, dest, source.errorString());
0304             return false;
0305         }
0306         QFile destFile(dest);
0307         destFile.setPermissions(destFile.permissions() | QFile::ExeOwner);
0308     } else {
0309         if (binaryPackages->contains(QMimeDatabase().mimeTypeForFile(archive).name())) {
0310             packageKit(PackageOperation::Install, archive);
0311         }
0312         const QString dir = generateDirPath(archive);
0313         if (QFile::exists(dir)) {
0314             if (!QDir(dir).removeRecursively()) {
0315                 errorText = i18n("Failed to remove directory %1", dir);
0316                 return false;
0317             }
0318         }
0319 
0320         if (QDir().mkdir(dir)) {
0321             errorText = i18n("Failed to create directory %1", dir);
0322         }
0323 
0324         runUncompress(archive, dir);
0325 
0326         // Try "install-it" first
0327         QString installItPath;
0328         const QStringList basenames1 = {"install-it.sh", "install-it"};
0329         for (const auto &basename : basenames1) {
0330             const auto path = findRecursive(dir, basename);
0331             if (!path.isEmpty()) {
0332                 installItPath = path;
0333                 break;
0334             }
0335         }
0336 
0337         if (!installItPath.isEmpty()) {
0338             return runScriptVariants(installItPath, false, QStringList{}, errorText);
0339         }
0340 
0341         // If "install-it" is missing, try "install"
0342         QString installerPath;
0343         const QStringList basenames2 = {"installKDE4.sh", "installKDE4", "install.sh", "install*.sh"};
0344         for (const auto &basename : basenames2) {
0345             const auto path = findRecursive(dir, basename);
0346             if (!path.isEmpty()) {
0347                 installerPath = path;
0348                 break;
0349             }
0350         }
0351 
0352         if (!installerPath.isEmpty()) {
0353             // Try to run script without variants first
0354             if (!runScriptVariants(installerPath, false, {}, errorText)) {
0355                 return runScriptVariants(installerPath, true, {"--local", "--local-install", "--install"}, errorText);
0356             }
0357             return true;
0358         }
0359 
0360         fail(i18n("Failed to find an installation script in %1", dir));
0361     }
0362 
0363     return true;
0364 }
0365 
0366 bool cmdUninstall(const QString &archive, QString &errorText)
0367 {
0368     const auto serviceDir = getServiceMenusDir();
0369     if (archive.endsWith(QLatin1String(".desktop"))) {
0370         // Append basename to destination directory
0371         const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
0372         QFile file(dest);
0373         if (!file.remove()) {
0374             errorText = i18n("Failed to remove .desktop file %1: %2", dest, file.errorString());
0375             return false;
0376         }
0377     } else {
0378         if (binaryPackages->contains(QMimeDatabase().mimeTypeForFile(archive).name())) {
0379             packageKit(PackageOperation::Uninstall, archive);
0380         }
0381         const QString dir = generateDirPath(archive);
0382 
0383         // Try "deinstall" first
0384         QString deinstallPath;
0385         const QStringList basenames1 = {"uninstall.sh", "uninstall", "deinstall.sh", "deinstall"};
0386         for (const auto &basename : basenames1) {
0387             const auto path = findRecursive(dir, basename);
0388             if (!path.isEmpty()) {
0389                 deinstallPath = path;
0390                 break;
0391             }
0392         }
0393 
0394         if (!deinstallPath.isEmpty()) {
0395             const bool ok = runScriptVariants(deinstallPath, false, {}, errorText);
0396             if (!ok) {
0397                 return ok;
0398             }
0399         } else {
0400             // If "deinstall" is missing, try "install --uninstall"
0401             QString installerPath;
0402             const QStringList basenames2 = {"install-it.sh", "install-it", "installKDE4.sh", "installKDE4", "install.sh", "install"};
0403             for (const auto &basename : basenames2) {
0404                 const auto path = findRecursive(dir, basename);
0405                 if (!path.isEmpty()) {
0406                     installerPath = path;
0407                     break;
0408                 }
0409             }
0410 
0411             if (!installerPath.isEmpty()) {
0412                 const bool ok = runScriptVariants(installerPath, true, {"--remove", "--delete", "--uninstall", "--deinstall"}, errorText);
0413                 if (!ok) {
0414                     return ok;
0415                 }
0416             } else {
0417                 fail(i18n("Failed to find an uninstallation script in %1", dir));
0418             }
0419         }
0420 
0421         QDir dirObject(dir);
0422         if (!dirObject.removeRecursively()) {
0423             errorText = i18n("Failed to remove directory %1", dir);
0424             return false;
0425         }
0426     }
0427 
0428     return true;
0429 }
0430 
0431 int main(int argc, char *argv[])
0432 {
0433     QGuiApplication app(argc, argv);
0434 
0435     QCommandLineParser parser;
0436     parser.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall."));
0437     parser.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive."));
0438     parser.process(app);
0439 
0440     const QStringList args = parser.positionalArguments();
0441     if (args.isEmpty()) {
0442         fail(i18n("Command is required."));
0443     }
0444     if (args.size() == 1) {
0445         fail(i18n("Path to archive is required."));
0446     }
0447 
0448     const QString cmd = args[0];
0449     const QString archive = args[1];
0450 
0451     QString errorText;
0452     if (cmd == QLatin1String("install")) {
0453         if (!cmdInstall(archive, errorText)) {
0454             fail(errorText);
0455         }
0456     } else if (cmd == QLatin1String("uninstall")) {
0457         if (!cmdUninstall(archive, errorText)) {
0458             fail(errorText);
0459         }
0460     } else {
0461         fail(i18n("Unsupported command %1", cmd));
0462     }
0463 
0464     return 0;
0465 }