]> cloud.milkyroute.net Git - dolphin.git/blob - src/settings/contextmenu/servicemenuinstaller/servicemenuinstaller.cpp
Merge branch 'release/22.04'
[dolphin.git] / src / settings / contextmenu / servicemenuinstaller / servicemenuinstaller.cpp
1 /*
2 * SPDX-FileCopyrightText: 2019 Alexander Potashev <aspotashev@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5 */
6
7 #include <QDebug>
8 #include <QProcess>
9 #include <QTimer>
10 #include <QStandardPaths>
11 #include <QDir>
12 #include <QDirIterator>
13 #include <QCommandLineParser>
14 #include <QMimeDatabase>
15 #include <QUrl>
16 #include <QGuiApplication>
17 #include <KLocalizedString>
18 #include <KShell>
19
20 #include "../../../config-packagekit.h"
21
22 Q_GLOBAL_STATIC_WITH_ARGS(QStringList, binaryPackages, ({QLatin1String("application/vnd.debian.binary-package"),
23 QLatin1String("application/x-rpm"),
24 QLatin1String("application/x-xz"),
25 QLatin1String("application/zstd")}))
26
27 enum PackageOperation {
28 Install,
29 Uninstall
30 };
31
32 #ifdef HAVE_PACKAGEKIT
33 #include <PackageKit/Daemon>
34 #include <PackageKit/Details>
35 #include <PackageKit/Transaction>
36 #else
37 #include <QDesktopServices>
38 #endif
39
40 // @param msg Error that gets logged to CLI
41 Q_NORETURN void fail(const QString &str)
42 {
43 qCritical() << str;
44 const QStringList args = {"--detailederror" ,i18n("Dolphin service menu installation failed"), str};
45 QProcess::startDetached("kdialog", args);
46
47 exit(1);
48 }
49
50 QString getServiceMenusDir()
51 {
52 const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
53 return QDir(dataLocation).absoluteFilePath("kio/servicemenus");
54 }
55
56 #ifdef HAVE_PACKAGEKIT
57 void packageKitInstall(const QString &fileName)
58 {
59 PackageKit::Transaction *transaction = PackageKit::Daemon::installFile(fileName, PackageKit::Transaction::TransactionFlagNone);
60
61 const auto exitWithError = [=](PackageKit::Transaction::Error, const QString &details) {
62 fail(details);
63 };
64
65 QObject::connect(transaction, &PackageKit::Transaction::finished,
66 [=](PackageKit::Transaction::Exit status, uint) {
67 if (status == PackageKit::Transaction::ExitSuccess) {
68 exit(0);
69 }
70 // Fallback error handling
71 QTimer::singleShot(500, [=](){
72 fail(i18n("Failed to install \"%1\", exited with status \"%2\"",
73 fileName, QVariant::fromValue(status).toString()));
74 });
75 });
76 QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
77 }
78
79 void packageKitUninstall(const QString &fileName)
80 {
81 const auto exitWithError = [=](PackageKit::Transaction::Error, const QString &details) {
82 fail(details);
83 };
84 const auto uninstallLambda = [=](PackageKit::Transaction::Exit status, uint) {
85 if (status == PackageKit::Transaction::ExitSuccess) {
86 exit(0);
87 }
88 };
89
90 PackageKit::Transaction *transaction = PackageKit::Daemon::getDetailsLocal(fileName);
91 QObject::connect(transaction, &PackageKit::Transaction::details,
92 [=](const PackageKit::Details &details) {
93 PackageKit::Transaction *transaction = PackageKit::Daemon::removePackage(details.packageId());
94 QObject::connect(transaction, &PackageKit::Transaction::finished, uninstallLambda);
95 QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
96 });
97
98 QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
99 // Fallback error handling
100 QObject::connect(transaction, &PackageKit::Transaction::finished,
101 [=](PackageKit::Transaction::Exit status, uint) {
102 if (status != PackageKit::Transaction::ExitSuccess) {
103 QTimer::singleShot(500, [=]() {
104 fail(i18n("Failed to uninstall \"%1\", exited with status \"%2\"",
105 fileName, QVariant::fromValue(status).toString()));
106 });
107 }
108 });
109 }
110 #endif
111
112 Q_NORETURN void packageKit(PackageOperation operation, const QString &fileName)
113 {
114 #ifdef HAVE_PACKAGEKIT
115 QFileInfo fileInfo(fileName);
116 if (!fileInfo.exists()) {
117 fail(i18n("The file does not exist!"));
118 }
119 const QString absPath = fileInfo.absoluteFilePath();
120 if (operation == PackageOperation::Install) {
121 packageKitInstall(absPath);
122 } else {
123 packageKitUninstall(absPath);
124 }
125 QGuiApplication::exec(); // For event handling, no return after signals finish
126 fail(i18n("Unknown error when installing package"));
127 #else
128 Q_UNUSED(operation)
129 QDesktopServices::openUrl(QUrl(fileName));
130 exit(0);
131 #endif
132 }
133
134 struct UncompressCommand
135 {
136 QString command;
137 QStringList args1;
138 QStringList args2;
139 };
140
141 enum ScriptExecution{
142 Process,
143 Konsole
144 };
145
146 void runUncompress(const QString &inputPath, const QString &outputPath)
147 {
148 QVector<QPair<QStringList, UncompressCommand>> mimeTypeToCommand;
149 mimeTypeToCommand.append({{"application/x-tar", "application/tar", "application/x-gtar", "multipart/x-tar"},
150 UncompressCommand({"tar", {"-xf"}, {"-C"}})});
151 mimeTypeToCommand.append({{"application/x-gzip", "application/gzip",
152 "application/x-gzip-compressed-tar", "application/gzip-compressed-tar",
153 "application/x-gzip-compressed", "application/gzip-compressed",
154 "application/tgz", "application/x-compressed-tar",
155 "application/x-compressed-gtar", "file/tgz",
156 "multipart/x-tar-gz", "application/x-gunzip", "application/gzipped",
157 "gzip/document"},
158 UncompressCommand({"tar", {"-zxf"}, {"-C"}})});
159 mimeTypeToCommand.append({{"application/bzip", "application/bzip2", "application/x-bzip",
160 "application/x-bzip2", "application/bzip-compressed",
161 "application/bzip2-compressed", "application/x-bzip-compressed",
162 "application/x-bzip2-compressed", "application/bzip-compressed-tar",
163 "application/bzip2-compressed-tar", "application/x-bzip-compressed-tar",
164 "application/x-bzip2-compressed-tar", "application/x-bz2"},
165 UncompressCommand({"tar", {"-jxf"}, {"-C"}})});
166 mimeTypeToCommand.append({{"application/zip", "application/x-zip", "application/x-zip-compressed",
167 "multipart/x-zip"},
168 UncompressCommand({"unzip", {}, {"-d"}})});
169
170 const auto mime = QMimeDatabase().mimeTypeForFile(inputPath).name();
171
172 UncompressCommand command{};
173 for (const auto &pair : qAsConst(mimeTypeToCommand)) {
174 if (pair.first.contains(mime)) {
175 command = pair.second;
176 break;
177 }
178 }
179
180 if (command.command.isEmpty()) {
181 fail(i18n("Unsupported archive type %1: %2", mime, inputPath));
182 }
183
184 QProcess process;
185 process.start(
186 command.command,
187 QStringList() << command.args1 << inputPath << command.args2 << outputPath,
188 QIODevice::NotOpen);
189 if (!process.waitForStarted()) {
190 fail(i18n("Failed to run uncompressor command for %1", inputPath));
191 }
192
193 if (!process.waitForFinished()) {
194 fail(
195 i18n("Process did not finish in reasonable time: %1 %2", process.program(), process.arguments().join(" ")));
196 }
197
198 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
199 fail(i18n("Failed to uncompress %1", inputPath));
200 }
201 }
202
203 QString findRecursive(const QString &dir, const QString &basename)
204 {
205 QDirIterator it(dir, QStringList{basename}, QDir::Files, QDirIterator::Subdirectories);
206 while (it.hasNext()) {
207 return QFileInfo(it.next()).canonicalFilePath();
208 }
209
210 return QString();
211 }
212
213 bool runScriptOnce(const QString &path, const QStringList &args, ScriptExecution execution)
214 {
215 QProcess process;
216 process.setWorkingDirectory(QFileInfo(path).absolutePath());
217
218 const static bool konsoleAvailable = !QStandardPaths::findExecutable("konsole").isEmpty();
219 if (konsoleAvailable && execution == ScriptExecution::Konsole) {
220 QString bashCommand = KShell::quoteArg(path) + ' ';
221 if (!args.isEmpty()) {
222 bashCommand.append(args.join(' '));
223 }
224 bashCommand.append("|| $SHELL");
225 // If the install script fails a shell opens and the user can fix the problem
226 // without an error konsole closes
227 process.start("konsole", QStringList() << "-e" << "bash" << "-c" << bashCommand, QIODevice::NotOpen);
228 } else {
229 process.start(path, args, QIODevice::NotOpen);
230 }
231 if (!process.waitForStarted()) {
232 fail(i18n("Failed to run installer script %1", path));
233 }
234
235 // Wait until installer exits, without timeout
236 if (!process.waitForFinished(-1)) {
237 qWarning() << "Failed to wait on installer:" << process.program() << process.arguments().join(" ");
238 return false;
239 }
240
241 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
242 qWarning() << "Installer script exited with error:" << process.program() << process.arguments().join(" ");
243 return false;
244 }
245
246 return true;
247 }
248
249 // If hasArgVariants is true, run "path".
250 // If hasArgVariants is false, run "path argVariants[i]" until successful.
251 bool runScriptVariants(const QString &path, bool hasArgVariants, const QStringList &argVariants, QString &errorText)
252 {
253 QFile file(path);
254 if (!file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) {
255 errorText = i18n("Failed to set permissions on %1: %2", path, file.errorString());
256 return false;
257 }
258
259 qInfo() << "[servicemenuinstaller]: Trying to run installer/uninstaller" << path;
260 if (hasArgVariants) {
261 for (const auto &arg : argVariants) {
262 if (runScriptOnce(path, {arg}, ScriptExecution::Process)) {
263 return true;
264 }
265 }
266 } else if (runScriptOnce(path, {}, ScriptExecution::Konsole)) {
267 return true;
268 }
269
270 errorText = i18nc(
271 "%2 = comma separated list of arguments",
272 "Installer script %1 failed, tried arguments \"%2\".", path, argVariants.join(i18nc("Separator between arguments", "\", \"")));
273 return false;
274 }
275
276 QString generateDirPath(const QString &archive)
277 {
278 return QStringLiteral("%1-dir").arg(archive);
279 }
280
281 bool cmdInstall(const QString &archive, QString &errorText)
282 {
283 const auto serviceDir = getServiceMenusDir();
284 if (!QDir().mkpath(serviceDir)) {
285 // TODO Cannot get error string because of this bug: https://bugreports.qt.io/browse/QTBUG-1483
286 errorText = i18n("Failed to create path %1", serviceDir);
287 return false;
288 }
289
290 if (archive.endsWith(QLatin1String(".desktop"))) {
291 // Append basename to destination directory
292 const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
293 if (QFileInfo::exists(dest)) {
294 QFile::remove(dest);
295 }
296 qInfo() << "Single-File Service-Menu" << archive << dest;
297
298 QFile source(archive);
299 if (!source.copy(dest)) {
300 errorText = i18n("Failed to copy .desktop file %1 to %2: %3", archive, dest, source.errorString());
301 return false;
302 }
303 QFile destFile(dest);
304 destFile.setPermissions(destFile.permissions() | QFile::ExeOwner);
305 } else {
306 if (binaryPackages->contains(QMimeDatabase().mimeTypeForFile(archive).name())) {
307 packageKit(PackageOperation::Install, archive);
308 }
309 const QString dir = generateDirPath(archive);
310 if (QFile::exists(dir)) {
311 if (!QDir(dir).removeRecursively()) {
312 errorText = i18n("Failed to remove directory %1", dir);
313 return false;
314 }
315 }
316
317 if (QDir().mkdir(dir)) {
318 errorText = i18n("Failed to create directory %1", dir);
319 }
320
321 runUncompress(archive, dir);
322
323 // Try "install-it" first
324 QString installItPath;
325 const QStringList basenames1 = {"install-it.sh", "install-it"};
326 for (const auto &basename : basenames1) {
327 const auto path = findRecursive(dir, basename);
328 if (!path.isEmpty()) {
329 installItPath = path;
330 break;
331 }
332 }
333
334 if (!installItPath.isEmpty()) {
335 return runScriptVariants(installItPath, false, QStringList{}, errorText);
336 }
337
338 // If "install-it" is missing, try "install"
339 QString installerPath;
340 const QStringList basenames2 = {"installKDE4.sh", "installKDE4", "install.sh", "install"};
341 for (const auto &basename : basenames2) {
342 const auto path = findRecursive(dir, basename);
343 if (!path.isEmpty()) {
344 installerPath = path;
345 break;
346 }
347 }
348
349 if (!installerPath.isEmpty()) {
350 // Try to run script without variants first
351 if (!runScriptVariants(installerPath, false, {}, errorText)) {
352 return runScriptVariants(installerPath, true, {"--local", "--local-install", "--install"}, errorText);
353 }
354 return true;
355 }
356
357 fail(i18n("Failed to find an installation script in %1", dir));
358 }
359
360 return true;
361 }
362
363 bool cmdUninstall(const QString &archive, QString &errorText)
364 {
365 const auto serviceDir = getServiceMenusDir();
366 if (archive.endsWith(QLatin1String(".desktop"))) {
367 // Append basename to destination directory
368 const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
369 QFile file(dest);
370 if (!file.remove()) {
371 errorText = i18n("Failed to remove .desktop file %1: %2", dest, file.errorString());
372 return false;
373 }
374 } else {
375 if (binaryPackages->contains(QMimeDatabase().mimeTypeForFile(archive).name())) {
376 packageKit(PackageOperation::Uninstall, archive);
377 }
378 const QString dir = generateDirPath(archive);
379
380 // Try "deinstall" first
381 QString deinstallPath;
382 const QStringList basenames1 = {"uninstall.sh", "uninstal", "deinstall.sh", "deinstall"};
383 for (const auto &basename : basenames1) {
384 const auto path = findRecursive(dir, basename);
385 if (!path.isEmpty()) {
386 deinstallPath = path;
387 break;
388 }
389 }
390
391 if (!deinstallPath.isEmpty()) {
392 const bool ok = runScriptVariants(deinstallPath, false, {}, errorText);
393 if (!ok) {
394 return ok;
395 }
396 } else {
397 // If "deinstall" is missing, try "install --uninstall"
398 QString installerPath;
399 const QStringList basenames2 = {"install-it.sh", "install-it", "installKDE4.sh",
400 "installKDE4", "install.sh", "install"};
401 for (const auto &basename : basenames2) {
402 const auto path = findRecursive(dir, basename);
403 if (!path.isEmpty()) {
404 installerPath = path;
405 break;
406 }
407 }
408
409 if (!installerPath.isEmpty()) {
410 const bool ok = runScriptVariants(installerPath, true,
411 {"--remove", "--delete", "--uninstall", "--deinstall"}, errorText);
412 if (!ok) {
413 return ok;
414 }
415 } else {
416 fail(i18n("Failed to find an uninstallation script in %1", dir));
417 }
418 }
419
420 QDir dirObject(dir);
421 if (!dirObject.removeRecursively()) {
422 errorText = i18n("Failed to remove directory %1", dir);
423 return false;
424 }
425 }
426
427 return true;
428 }
429
430 int main(int argc, char *argv[])
431 {
432 QGuiApplication app(argc, argv);
433
434 QCommandLineParser parser;
435 parser.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall."));
436 parser.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive."));
437 parser.process(app);
438
439 const QStringList args = parser.positionalArguments();
440 if (args.isEmpty()) {
441 fail(i18n("Command is required."));
442 }
443 if (args.size() == 1) {
444 fail(i18n("Path to archive is required."));
445 }
446
447 const QString cmd = args[0];
448 const QString archive = args[1];
449
450 QString errorText;
451 if (cmd == QLatin1String("install")) {
452 if (!cmdInstall(archive, errorText)) {
453 fail(errorText);
454 }
455 } else if (cmd == QLatin1String("uninstall")) {
456 if (!cmdUninstall(archive, errorText)) {
457 fail(errorText);
458 }
459 } else {
460 fail(i18n("Unsupported command %1", cmd));
461 }
462
463 return 0;
464 }