1/*
2 This file is part of KNewStuff2.
3 SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org>
4 SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.1-or-later
7*/
8
9#include "installation_p.h"
10
11#include <QDesktopServices>
12#include <QDir>
13#include <QFile>
14#include <QProcess>
15#include <QTemporaryFile>
16#include <QUrlQuery>
17
18#include "karchive.h"
19#include "knewstuff_version.h"
20#include "qmimedatabase.h"
21#include <KRandom>
22#include <KShell>
23#include <KTar>
24#include <KZip>
25
26#include <KPackage/Package>
27#include <KPackage/PackageJob>
28
29#include <KLocalizedString>
30#include <knewstuffcore_debug.h>
31#include <qstandardpaths.h>
32
33#include "jobs/filecopyjob.h"
34#include "question.h"
35#ifdef Q_OS_WIN
36#include <shlobj.h>
37#include <windows.h>
38#endif
39
40using namespace KNSCore;
41
42Installation::Installation(QObject *parent)
43 : QObject(parent)
44{
45}
46
47bool Installation::readConfig(const KConfigGroup &group, QString &errorMessage)
48{
49 // FIXME: add support for several categories later on
50 const QString uncompression = group.readEntry(key: "Uncompress", QStringLiteral("never"));
51 if (uncompression == QLatin1String("always") || uncompression == QLatin1String("true")) {
52 uncompressSetting = AlwaysUncompress;
53 } else if (uncompression == QLatin1String("archive")) {
54 uncompressSetting = UncompressIfArchive;
55 } else if (uncompression == QLatin1String("subdir")) {
56 uncompressSetting = UncompressIntoSubdir;
57 } else if (uncompression == QLatin1String("kpackage")) {
58 uncompressSetting = UseKPackageUncompression;
59 } else if (uncompression == QLatin1String("subdir-archive")) {
60 uncompressSetting = UncompressIntoSubdirIfArchive;
61 } else if (uncompression == QLatin1String("never")) {
62 uncompressSetting = NeverUncompress;
63 } else {
64 errorMessage = QStringLiteral("invalid Uncompress setting chosen, must be one of: subdir, always, archive, never, or kpackage");
65 qCCritical(KNEWSTUFFCORE) << errorMessage;
66 return false;
67 }
68
69 kpackageStructure = group.readEntry(key: "KPackageStructure");
70 if (uncompressSetting == UseKPackageUncompression && kpackageStructure.isEmpty()) {
71 errorMessage = QStringLiteral("kpackage uncompress setting chosen, but no KPackageStructure specified");
72 qCCritical(KNEWSTUFFCORE) << errorMessage;
73 return false;
74 }
75
76 postInstallationCommand = group.readEntry(key: "InstallationCommand");
77 uninstallCommand = group.readEntry(key: "UninstallCommand");
78 standardResourceDirectory = group.readEntry(key: "StandardResource");
79 targetDirectory = group.readEntry(key: "TargetDir");
80 xdgTargetDirectory = group.readEntry(key: "XdgTargetDir");
81
82 installPath = group.readEntry(key: "InstallPath");
83 absoluteInstallPath = group.readEntry(key: "AbsoluteInstallPath");
84
85 if (standardResourceDirectory.isEmpty() && targetDirectory.isEmpty() && xdgTargetDirectory.isEmpty() && installPath.isEmpty()
86 && absoluteInstallPath.isEmpty()) {
87 qCCritical(KNEWSTUFFCORE) << "No installation target set";
88 return false;
89 }
90 return true;
91}
92
93void Installation::install(const Entry &entry)
94{
95 downloadPayload(entry);
96}
97
98void Installation::downloadPayload(const KNSCore::Entry &entry)
99{
100 if (!entry.isValid()) {
101 Q_EMIT signalInstallationFailed(i18n("Invalid item."), entry);
102 return;
103 }
104 QUrl source = QUrl(entry.payload());
105
106 if (!source.isValid()) {
107 qCCritical(KNEWSTUFFCORE) << "The entry doesn't have a payload.";
108 Q_EMIT signalInstallationFailed(i18n("Download of item failed: no download URL for \"%1\".", entry.name()), entry);
109 return;
110 }
111
112 QString fileName(source.fileName());
113 QTemporaryFile tempFile(QDir::tempPath() + QStringLiteral("/XXXXXX-") + fileName);
114 tempFile.setAutoRemove(false);
115 if (!tempFile.open()) {
116 return; // ERROR
117 }
118 QUrl destination = QUrl::fromLocalFile(localfile: tempFile.fileName());
119 qCDebug(KNEWSTUFFCORE) << "Downloading payload" << source << "to" << destination;
120#ifdef Q_OS_WIN // can't write to the file if it's open, on Windows
121 tempFile.close();
122#endif
123
124 // FIXME: check for validity
125 FileCopyJob *job = FileCopyJob::file_copy(source, destination, permissions: -1, flags: JobFlag::Overwrite | JobFlag::HideProgressInfo);
126 connect(sender: job, signal: &KJob::result, context: this, slot: &Installation::slotPayloadResult);
127
128 entry_jobs[job] = entry;
129}
130
131void Installation::slotPayloadResult(KJob *job)
132{
133 // for some reason this slot is getting called 3 times on one job error
134 if (entry_jobs.contains(key: job)) {
135 Entry entry = entry_jobs[job];
136 entry_jobs.remove(key: job);
137
138 if (job->error()) {
139 const QString errorMessage = i18n("Download of \"%1\" failed, error: %2", entry.name(), job->errorString());
140 qCWarning(KNEWSTUFFCORE) << errorMessage;
141 Q_EMIT signalInstallationFailed(message: errorMessage, entry);
142 } else {
143 FileCopyJob *fcjob = static_cast<FileCopyJob *>(job);
144 qCDebug(KNEWSTUFFCORE) << "Copied to" << fcjob->destUrl();
145 QMimeDatabase db;
146 QMimeType mimeType = db.mimeTypeForFile(fileName: fcjob->destUrl().toLocalFile());
147 if (mimeType.inherits(QStringLiteral("text/html")) || mimeType.inherits(QStringLiteral("application/x-php"))) {
148 const auto error = i18n("Cannot install '%1' because it points to a web page. Click <a href='%2'>here</a> to finish the installation.",
149 entry.name(),
150 fcjob->srcUrl().toString());
151 Q_EMIT signalInstallationFailed(message: error, entry);
152 entry.setStatus(KNSCore::Entry::Invalid);
153 Q_EMIT signalEntryChanged(entry);
154 return;
155 }
156
157 Q_EMIT signalPayloadLoaded(payload: fcjob->destUrl());
158 install(entry, downloadedFile: fcjob->destUrl().toLocalFile());
159 }
160 }
161}
162
163void KNSCore::Installation::install(KNSCore::Entry entry, const QString &downloadedFile)
164{
165 qCWarning(KNEWSTUFFCORE) << "Install:" << entry.name() << "from" << downloadedFile;
166 Q_ASSERT(QFileInfo::exists(downloadedFile));
167
168 if (entry.payload().isEmpty()) {
169 qCDebug(KNEWSTUFFCORE) << "No payload associated with:" << entry.name();
170 return;
171 }
172
173 // TODO Add async checksum verification
174
175 QString targetPath = targetInstallationPath();
176 QStringList installedFiles = installDownloadedFileAndUncompress(entry, payloadfile: downloadedFile, installdir: targetPath);
177
178 if (uncompressionSetting() != UseKPackageUncompression) {
179 if (installedFiles.isEmpty()) {
180 if (entry.status() == KNSCore::Entry::Installing) {
181 entry.setStatus(KNSCore::Entry::Downloadable);
182 } else if (entry.status() == KNSCore::Entry::Updating) {
183 entry.setStatus(KNSCore::Entry::Updateable);
184 }
185 Q_EMIT signalEntryChanged(entry);
186 Q_EMIT signalInstallationFailed(i18n("Could not install \"%1\": file not found.", entry.name()), entry);
187 return;
188 }
189
190 entry.setInstalledFiles(installedFiles);
191
192 auto installationFinished = [this, entry]() {
193 Entry newentry = entry;
194 if (!newentry.updateVersion().isEmpty()) {
195 newentry.setVersion(newentry.updateVersion());
196 }
197 if (newentry.updateReleaseDate().isValid()) {
198 newentry.setReleaseDate(newentry.updateReleaseDate());
199 }
200 newentry.setStatus(KNSCore::Entry::Installed);
201 Q_EMIT signalEntryChanged(entry: newentry);
202 Q_EMIT signalInstallationFinished(entry: newentry);
203 };
204 if (!postInstallationCommand.isEmpty()) {
205 QString scriptArgPath = !installedFiles.isEmpty() ? installedFiles.first() : targetPath;
206 if (scriptArgPath.endsWith(c: QLatin1Char('*'))) {
207 scriptArgPath = scriptArgPath.left(n: scriptArgPath.lastIndexOf(c: QLatin1Char('*')));
208 }
209 QProcess *p = runPostInstallationCommand(installPath: scriptArgPath, entry);
210 connect(sender: p, signal: &QProcess::finished, context: this, slot: [entry, installationFinished, this](int exitCode) {
211 if (exitCode) {
212 Entry newEntry = entry;
213 newEntry.setStatus(KNSCore::Entry::Invalid);
214 Q_EMIT signalEntryChanged(entry: newEntry);
215 } else {
216 installationFinished();
217 }
218 });
219 } else {
220 installationFinished();
221 }
222 }
223}
224
225QString Installation::targetInstallationPath() const
226{
227 // installdir is the target directory
228 QString installdir;
229
230 const bool userScope = true;
231 // installpath also contains the file name if it's a single file, otherwise equal to installdir
232 int pathcounter = 0;
233 // wallpaper is already managed in the case of !xdgTargetDirectory.isEmpty()
234 if (!standardResourceDirectory.isEmpty() && standardResourceDirectory != QLatin1String("wallpaper")) {
235 QStandardPaths::StandardLocation location = QStandardPaths::TempLocation;
236 // crude translation KStandardDirs names -> QStandardPaths enum
237 if (standardResourceDirectory == QLatin1String("tmp")) {
238 location = QStandardPaths::TempLocation;
239 } else if (standardResourceDirectory == QLatin1String("config")) {
240 location = QStandardPaths::ConfigLocation;
241 }
242
243 if (userScope) {
244 installdir = QStandardPaths::writableLocation(type: location);
245 } else { // system scope
246 installdir = QStandardPaths::standardLocations(type: location).constLast();
247 }
248 pathcounter++;
249 }
250 if (!targetDirectory.isEmpty() && targetDirectory != QLatin1String("/")) {
251 if (userScope) {
252 installdir = QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QLatin1Char('/') + targetDirectory + QLatin1Char('/');
253 } else { // system scope
254 installdir = QStandardPaths::locate(type: QStandardPaths::GenericDataLocation, fileName: targetDirectory, options: QStandardPaths::LocateDirectory) + QLatin1Char('/');
255 }
256 pathcounter++;
257 }
258 if (!xdgTargetDirectory.isEmpty() && xdgTargetDirectory != QLatin1String("/")) {
259 installdir = QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QLatin1Char('/') + xdgTargetDirectory + QLatin1Char('/');
260 pathcounter++;
261 }
262 if (!installPath.isEmpty()) {
263#if defined(Q_OS_WIN)
264 WCHAR wPath[MAX_PATH + 1];
265 if (SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, wPath) == S_OK) {
266 installdir = QString::fromUtf16((const char16_t *)wPath) + QLatin1Char('/') + installPath + QLatin1Char('/');
267 } else {
268 installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/');
269 }
270#else
271 installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/');
272#endif
273 pathcounter++;
274 }
275 if (!absoluteInstallPath.isEmpty()) {
276 installdir = absoluteInstallPath + QLatin1Char('/');
277 pathcounter++;
278 }
279
280 if (pathcounter != 1) {
281 qCCritical(KNEWSTUFFCORE) << "Wrong number of installation directories given.";
282 return QString();
283 }
284
285 qCDebug(KNEWSTUFFCORE) << "installdir: " << installdir;
286
287 // create the dir if it doesn't exist (QStandardPaths doesn't create it, unlike KStandardDirs!)
288 QDir().mkpath(dirPath: installdir);
289
290 return installdir;
291}
292
293QStringList Installation::installDownloadedFileAndUncompress(const KNSCore::Entry &entry, const QString &payloadfile, const QString installdir)
294{
295 // Collect all files that were installed
296 QStringList installedFiles;
297 bool isarchive = true;
298 UncompressionOptions uncompressionOpt = uncompressionSetting();
299
300 // respect the uncompress flag in the knsrc
301 if (uncompressionOpt == UseKPackageUncompression) {
302 qCDebug(KNEWSTUFFCORE) << "Using KPackage for installation";
303 auto resetEntryStatus = [this, entry]() {
304 KNSCore::Entry changedEntry(entry);
305 if (changedEntry.status() == KNSCore::Entry::Installing || changedEntry.status() == KNSCore::Entry::Installed) {
306 changedEntry.setStatus(KNSCore::Entry::Downloadable);
307 } else if (changedEntry.status() == KNSCore::Entry::Updating) {
308 changedEntry.setStatus(KNSCore::Entry::Updateable);
309 }
310 Q_EMIT signalEntryChanged(entry: changedEntry);
311 };
312
313 qCDebug(KNEWSTUFFCORE) << "About to attempt to install" << payloadfile << "as" << kpackageStructure;
314 auto job = KPackage::PackageJob::install(packageFormat: kpackageStructure, sourcePackage: payloadfile);
315 connect(sender: job, signal: &KPackage::PackageJob::finished, context: this, slot: [this, entry, payloadfile, resetEntryStatus, job]() {
316 if (job->error() == KJob::NoError) {
317 Entry newentry = entry;
318 newentry.setInstalledFiles(QStringList{job->package().path()});
319 // update version and release date to the new ones
320 if (newentry.status() == KNSCore::Entry::Updating) {
321 if (!newentry.updateVersion().isEmpty()) {
322 newentry.setVersion(newentry.updateVersion());
323 }
324 if (newentry.updateReleaseDate().isValid()) {
325 newentry.setReleaseDate(newentry.updateReleaseDate());
326 }
327 }
328 newentry.setStatus(KNSCore::Entry::Installed);
329 // We can remove the downloaded file, because we don't save its location and don't need it to uninstall the entry
330 QFile::remove(fileName: payloadfile);
331 Q_EMIT signalEntryChanged(entry: newentry);
332 Q_EMIT signalInstallationFinished(entry: newentry);
333 qCDebug(KNEWSTUFFCORE) << "Install job finished with no error and we now have files" << job->package().path();
334 } else {
335 if (job->error() == KPackage::PackageJob::JobError::NewerVersionAlreadyInstalledError) {
336 Entry newentry = entry;
337 newentry.setStatus(KNSCore::Entry::Installed);
338 newentry.setInstalledFiles(QStringList{job->package().path()});
339 // update version and release date to the new ones
340 if (!newentry.updateVersion().isEmpty()) {
341 newentry.setVersion(newentry.updateVersion());
342 }
343 if (newentry.updateReleaseDate().isValid()) {
344 newentry.setReleaseDate(newentry.updateReleaseDate());
345 }
346 Q_EMIT signalEntryChanged(entry: newentry);
347 Q_EMIT signalInstallationFinished(entry: newentry);
348 qCDebug(KNEWSTUFFCORE) << "Install job finished telling us this item was already installed with this version, so... let's "
349 "just make a small fib and say we totally installed that, honest, and we now have files"
350 << job->package().path();
351 } else {
352 Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2", payloadfile, job->errorText()), entry);
353 resetEntryStatus();
354 qCDebug(KNEWSTUFFCORE) << "Install job finished with error state" << job->error() << "and description" << job->error();
355 }
356 }
357 });
358 } else {
359 if (uncompressionOpt == AlwaysUncompress || uncompressionOpt == UncompressIntoSubdirIfArchive || uncompressionOpt == UncompressIfArchive
360 || uncompressionOpt == UncompressIntoSubdir) {
361 // this is weird but a decompression is not a single name, so take the path instead
362 QMimeDatabase db;
363 QMimeType mimeType = db.mimeTypeForFile(fileName: payloadfile);
364 qCDebug(KNEWSTUFFCORE) << "Postinstallation: uncompress the file";
365
366 // FIXME: check for overwriting, malicious archive entries (../foo) etc.
367 // FIXME: KArchive should provide "safe mode" for this!
368 QScopedPointer<KArchive> archive;
369
370 if (mimeType.inherits(QStringLiteral("application/zip"))) {
371 archive.reset(other: new KZip(payloadfile));
372 // clang-format off
373 } else if (mimeType.inherits(QStringLiteral("application/tar"))
374 || mimeType.inherits(QStringLiteral("application/x-tar")) // BUG 450662
375 || mimeType.inherits(QStringLiteral("application/x-gzip"))
376 || mimeType.inherits(QStringLiteral("application/x-bzip"))
377 || mimeType.inherits(QStringLiteral("application/x-lzma"))
378 || mimeType.inherits(QStringLiteral("application/x-xz"))
379 || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar"))
380 || mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) {
381 // clang-format on
382 archive.reset(other: new KTar(payloadfile));
383 } else {
384 qCCritical(KNEWSTUFFCORE) << "Could not determine type of archive file" << payloadfile;
385 if (uncompressionOpt == AlwaysUncompress) {
386 Q_EMIT signalInstallationError(i18n("Could not determine the type of archive of the downloaded file %1", payloadfile), entry);
387 return QStringList();
388 }
389 isarchive = false;
390 }
391
392 if (isarchive) {
393 bool success = archive->open(mode: QIODevice::ReadOnly);
394 if (!success) {
395 qCCritical(KNEWSTUFFCORE) << "Cannot open archive file" << payloadfile;
396 if (uncompressionOpt == AlwaysUncompress) {
397 Q_EMIT signalInstallationError(
398 i18n("Failed to open the archive file %1. The reported error was: %2", payloadfile, archive->errorString()),
399 entry);
400 return QStringList();
401 }
402 // otherwise, just copy the file
403 isarchive = false;
404 }
405
406 if (isarchive) {
407 const KArchiveDirectory *dir = archive->directory();
408 // if there is more than an item in the file, and we are requested to do so
409 // put contents in a subdirectory with the same name as the file
410 QString installpath;
411 const bool isSubdir =
412 (uncompressionOpt == UncompressIntoSubdir || uncompressionOpt == UncompressIntoSubdirIfArchive) && dir->entries().count() > 1;
413 if (isSubdir) {
414 installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName();
415 } else {
416 installpath = installdir;
417 }
418
419 if (dir->copyTo(dest: installpath)) {
420 // If we extracted the subdir we want to save it using the /* notation like we would when using the "archive" option
421 // Also if we use an (un)install command we only call it once with the folder as argument and not for each file
422 if (isSubdir) {
423 installedFiles << QDir(installpath).absolutePath() + QLatin1String("/*");
424 } else {
425 installedFiles << archiveEntries(path: installpath, dir);
426 }
427 } else {
428 qCWarning(KNEWSTUFFCORE) << "could not install" << entry.name() << "to" << installpath;
429 }
430
431 archive->close();
432 QFile::remove(fileName: payloadfile);
433 }
434 }
435 }
436
437 qCDebug(KNEWSTUFFCORE) << "isarchive:" << isarchive;
438
439 // some wallpapers are compressed, some aren't
440 if ((!isarchive && standardResourceDirectory == QLatin1String("wallpaper"))
441 || (uncompressionOpt == NeverUncompress || (uncompressionOpt == UncompressIfArchive && !isarchive)
442 || (uncompressionOpt == UncompressIntoSubdirIfArchive && !isarchive))) {
443 // no decompress but move to target
444
445 /// @todo when using KIO::get the http header can be accessed and it contains a real file name.
446 // FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names
447 QUrl source = QUrl(entry.payload());
448 qCDebug(KNEWSTUFFCORE) << "installing non-archive from" << source;
449 const QString installpath = QDir(installdir).filePath(fileName: source.fileName());
450
451 qCDebug(KNEWSTUFFCORE) << "Install to file" << installpath;
452 // FIXME: copy goes here (including overwrite checking)
453 // FIXME: what must be done now is to update the cache *again*
454 // in order to set the new payload filename (on root tag only)
455 // - this might or might not need to take uncompression into account
456 // FIXME: for updates, we might need to force an overwrite (that is, deleting before)
457 QFile file(payloadfile);
458 bool success = true;
459 const bool update = ((entry.status() == KNSCore::Entry::Updateable) || (entry.status() == KNSCore::Entry::Updating));
460
461 if (QFile::exists(fileName: installpath) && QDir::tempPath() != installdir) {
462 if (!update) {
463 Question question(Question::ContinueCancelQuestion);
464 question.setEntry(entry);
465 question.setQuestion(i18n("This file already exists on disk (possibly due to an earlier failed download attempt). Continuing means "
466 "overwriting it. Do you wish to overwrite the existing file?")
467 + QStringLiteral("\n'") + installpath + QLatin1Char('\''));
468 question.setTitle(i18n("Overwrite File"));
469 if (question.ask() != Question::ContinueResponse) {
470 return QStringList();
471 }
472 }
473 success = QFile::remove(fileName: installpath);
474 }
475 if (success) {
476 // remove in case it's already present and in a temporary directory, so we get to actually use the path again
477 if (installpath.startsWith(s: QDir::tempPath())) {
478 QFile::remove(fileName: installpath);
479 }
480 success = file.rename(newName: installpath);
481 qCWarning(KNEWSTUFFCORE) << "move:" << file.fileName() << "to" << installpath;
482 if (!success) {
483 qCWarning(KNEWSTUFFCORE) << file.errorString();
484 }
485 }
486 if (!success) {
487 Q_EMIT signalInstallationError(i18n("Unable to move the file %1 to the intended destination %2", payloadfile, installpath), entry);
488 qCCritical(KNEWSTUFFCORE) << "Cannot move file" << payloadfile << "to destination" << installpath;
489 return QStringList();
490 }
491 installedFiles << installpath;
492 }
493 }
494
495 return installedFiles;
496}
497
498QProcess *Installation::runPostInstallationCommand(const QString &installPath, const KNSCore::Entry &entry)
499{
500 QString command(postInstallationCommand);
501 QString fileArg(KShell::quoteArg(arg: installPath));
502 command.replace(before: QLatin1String("%f"), after: fileArg);
503
504 qCDebug(KNEWSTUFFCORE) << "Run command:" << command;
505
506 QProcess *ret = new QProcess(this);
507 auto onProcessFinished = [this, command, ret, entry](int exitcode, QProcess::ExitStatus status) {
508 const QString output{QString::fromLocal8Bit(ba: ret->readAllStandardError())};
509 if (status == QProcess::CrashExit) {
510 QString errorMessage = i18n("The installation failed while attempting to run the command:\n%1\n\nThe returned output was:\n%2", command, output);
511 Q_EMIT signalInstallationError(message: errorMessage, entry);
512 qCCritical(KNEWSTUFFCORE) << "Process crashed with command:" << command;
513 } else if (exitcode) {
514 // 130 means Ctrl+C as an exit code this is interpreted by KNewStuff as cancel operation
515 // and no error will be displayed to the user, BUG: 436355
516 if (exitcode == 130) {
517 qCCritical(KNEWSTUFFCORE) << "Command" << command << "failed was aborted by the user";
518 Q_EMIT signalInstallationFinished(entry);
519 } else {
520 Q_EMIT signalInstallationError(
521 i18n("The installation failed with code %1 while attempting to run the command:\n%2\n\nThe returned output was:\n%3",
522 exitcode,
523 command,
524 output),
525 entry);
526 qCCritical(KNEWSTUFFCORE) << "Command" << command << "failed with code" << exitcode;
527 }
528 }
529 sender()->deleteLater();
530 };
531 connect(sender: ret, signal: &QProcess::finished, context: this, slot&: onProcessFinished);
532
533 QStringList args = KShell::splitArgs(cmd: command);
534 ret->setProgram(args.takeFirst());
535 ret->setArguments(args);
536 ret->start();
537 return ret;
538}
539
540void Installation::uninstall(Entry entry)
541{
542 const auto deleteFilesAndMarkAsUninstalled = [entry, this]() {
543 bool deletionSuccessful = true;
544 const auto lst = entry.installedFiles();
545 for (const QString &file : lst) {
546 // This is used to delete the download location if there are no more entries
547 QFileInfo info(file);
548 if (info.isDir()) {
549 QDir().rmdir(dirName: file);
550 } else if (file.endsWith(s: QLatin1String("/*"))) {
551 QDir dir(file.left(n: file.size() - 2));
552 bool worked = dir.removeRecursively();
553 if (!worked) {
554 qCWarning(KNEWSTUFFCORE) << "Couldn't remove" << dir.path();
555 continue;
556 }
557 } else {
558 if (info.exists() || info.isSymLink()) {
559 bool worked = QFile::remove(fileName: file);
560 if (!worked) {
561 qWarning() << "unable to delete file " << file;
562 Q_EMIT signalInstallationFailed(
563 i18n("The removal of %1 failed, as the installed file %2 could not be automatically removed. You can attempt to manually delete "
564 "this file, if you believe this is an error.",
565 entry.name(),
566 file),
567 entry);
568 // Assume that the uninstallation has failed, and reset the entry to an installed state
569 deletionSuccessful = false;
570 break;
571 }
572 } else {
573 qWarning() << "unable to delete file " << file << ". file does not exist.";
574 }
575 }
576 }
577 Entry newEntry = entry;
578 if (deletionSuccessful) {
579 newEntry.setEntryDeleted();
580 } else {
581 newEntry.setStatus(KNSCore::Entry::Installed);
582 }
583
584 Q_EMIT signalEntryChanged(entry: newEntry);
585 };
586
587 if (uncompressionSetting() == UseKPackageUncompression) {
588 const auto lst = entry.installedFiles();
589 if (lst.length() == 1) {
590 const QString installedFile{lst.first()};
591
592 KJob *job = KPackage::PackageJob::uninstall(packageFormat: kpackageStructure, pluginId: installedFile);
593 connect(sender: job, signal: &KJob::result, context: this, slot: [this, installedFile, entry, job]() {
594 Entry newEntry = entry;
595 if (job->error() == KJob::NoError) {
596 newEntry.setEntryDeleted();
597 Q_EMIT signalEntryChanged(entry: newEntry);
598 } else {
599 Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2", installedFile, job->errorText()), entry);
600 }
601 });
602 }
603 deleteFilesAndMarkAsUninstalled();
604 } else {
605 const auto lst = entry.installedFiles();
606 // If there is an uninstall script, make sure it runs without errors
607 if (!uninstallCommand.isEmpty()) {
608 bool validFileExisted = false;
609 for (const QString &file : lst) {
610 QString filePath = file;
611 bool validFile = QFileInfo::exists(file: filePath);
612 // If we have uncompressed a subdir we write <path>/* in the config, but when calling a script
613 // we want to convert this to a normal path
614 if (!validFile && file.endsWith(c: QLatin1Char('*'))) {
615 filePath = filePath.left(n: filePath.lastIndexOf(c: QLatin1Char('*')));
616 validFile = QFileInfo::exists(file: filePath);
617 }
618 if (validFile) {
619 validFileExisted = true;
620 QString fileArg(KShell::quoteArg(arg: filePath));
621 QString command(uninstallCommand);
622 command.replace(before: QLatin1String("%f"), after: fileArg);
623
624 QStringList args = KShell::splitArgs(cmd: command);
625 const QString program = args.takeFirst();
626 QProcess *process = new QProcess(this);
627 process->start(program, arguments: args);
628 auto onProcessFinished = [this, command, process, entry, deleteFilesAndMarkAsUninstalled](int, QProcess::ExitStatus status) {
629 if (status == QProcess::CrashExit) {
630 const QString processOutput = QString::fromLocal8Bit(ba: process->readAllStandardError());
631 const QString err = i18n(
632 "The uninstallation process failed to successfully run the command %1\n"
633 "The output of was: \n%2\n"
634 "If you think this is incorrect, you can continue or cancel the uninstallation process",
635 KShell::quoteArg(command),
636 processOutput);
637 Q_EMIT signalInstallationError(message: err, entry);
638 // Ask the user if he wants to continue, even though the script failed
639 Question question(Question::ContinueCancelQuestion);
640 question.setEntry(entry);
641 question.setQuestion(err);
642 Question::Response response = question.ask();
643 if (response == Question::CancelResponse) {
644 // Use can delete files manually
645 Entry newEntry = entry;
646 newEntry.setStatus(KNSCore::Entry::Installed);
647 Q_EMIT signalEntryChanged(entry: newEntry);
648 return;
649 }
650 } else {
651 qCDebug(KNEWSTUFFCORE) << "Command executed successfully:" << command;
652 }
653 deleteFilesAndMarkAsUninstalled();
654 };
655 connect(sender: process, signal: &QProcess::finished, context: this, slot&: onProcessFinished);
656 }
657 }
658 // If the entry got deleted, but the RemoveDeadEntries option was not selected this case can happen
659 if (!validFileExisted) {
660 deleteFilesAndMarkAsUninstalled();
661 }
662 } else {
663 deleteFilesAndMarkAsUninstalled();
664 }
665 }
666}
667
668Installation::UncompressionOptions Installation::uncompressionSetting() const
669{
670 return uncompressSetting;
671}
672
673QStringList Installation::archiveEntries(const QString &path, const KArchiveDirectory *dir)
674{
675 QStringList files;
676 const auto lst = dir->entries();
677 for (const QString &entry : lst) {
678 const auto currentEntry = dir->entry(name: entry);
679
680 const QString childPath = QDir(path).filePath(fileName: entry);
681 if (currentEntry->isFile()) {
682 files << childPath;
683 } else if (currentEntry->isDirectory()) {
684 files << childPath + QStringLiteral("/*");
685 }
686 }
687 return files;
688}
689
690#include "moc_installation_p.cpp"
691

source code of knewstuff/src/core/installation.cpp