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 }