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