File indexing completed on 2024-04-21 03:56:23

0001 /*
0002     This file is part of KNewStuff2.
0003     SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org>
0004     SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.1-or-later
0007 */
0008 
0009 #include "installation_p.h"
0010 
0011 #include <QDesktopServices>
0012 #include <QDir>
0013 #include <QFile>
0014 #include <QProcess>
0015 #include <QTemporaryFile>
0016 #include <QUrlQuery>
0017 
0018 #include "karchive.h"
0019 #include "knewstuff_version.h"
0020 #include "qmimedatabase.h"
0021 #include <KRandom>
0022 #include <KShell>
0023 #include <KTar>
0024 #include <KZip>
0025 
0026 #include <KPackage/Package>
0027 #include <KPackage/PackageJob>
0028 
0029 #include <KLocalizedString>
0030 #include <knewstuffcore_debug.h>
0031 #include <qstandardpaths.h>
0032 
0033 #include "jobs/filecopyjob.h"
0034 #include "question.h"
0035 #ifdef Q_OS_WIN
0036 #include <shlobj.h>
0037 #include <windows.h>
0038 #endif
0039 
0040 using namespace KNSCore;
0041 
0042 Installation::Installation(QObject *parent)
0043     : QObject(parent)
0044 {
0045 }
0046 
0047 bool Installation::readConfig(const KConfigGroup &group, QString &errorMessage)
0048 {
0049     // FIXME: add support for several categories later on
0050     const QString uncompression = group.readEntry("Uncompress", QStringLiteral("never"));
0051     if (uncompression == QLatin1String("always") || uncompression == QLatin1String("true")) {
0052         uncompressSetting = AlwaysUncompress;
0053     } else if (uncompression == QLatin1String("archive")) {
0054         uncompressSetting = UncompressIfArchive;
0055     } else if (uncompression == QLatin1String("subdir")) {
0056         uncompressSetting = UncompressIntoSubdir;
0057     } else if (uncompression == QLatin1String("kpackage")) {
0058         uncompressSetting = UseKPackageUncompression;
0059     } else if (uncompression == QLatin1String("subdir-archive")) {
0060         uncompressSetting = UncompressIntoSubdirIfArchive;
0061     } else if (uncompression == QLatin1String("never")) {
0062         uncompressSetting = NeverUncompress;
0063     } else {
0064         errorMessage = QStringLiteral("invalid Uncompress setting chosen, must be one of: subdir, always, archive, never, or kpackage");
0065         qCCritical(KNEWSTUFFCORE) << errorMessage;
0066         return false;
0067     }
0068 
0069     kpackageStructure = group.readEntry("KPackageStructure");
0070     if (uncompressSetting == UseKPackageUncompression && kpackageStructure.isEmpty()) {
0071         errorMessage = QStringLiteral("kpackage uncompress setting chosen, but no KPackageStructure specified");
0072         qCCritical(KNEWSTUFFCORE) << errorMessage;
0073         return false;
0074     }
0075 
0076     postInstallationCommand = group.readEntry("InstallationCommand");
0077     uninstallCommand = group.readEntry("UninstallCommand");
0078     standardResourceDirectory = group.readEntry("StandardResource");
0079     targetDirectory = group.readEntry("TargetDir");
0080     xdgTargetDirectory = group.readEntry("XdgTargetDir");
0081 
0082     installPath = group.readEntry("InstallPath");
0083     absoluteInstallPath = group.readEntry("AbsoluteInstallPath");
0084 
0085     if (standardResourceDirectory.isEmpty() && targetDirectory.isEmpty() && xdgTargetDirectory.isEmpty() && installPath.isEmpty()
0086         && absoluteInstallPath.isEmpty()) {
0087         qCCritical(KNEWSTUFFCORE) << "No installation target set";
0088         return false;
0089     }
0090     return true;
0091 }
0092 
0093 void Installation::install(const Entry &entry)
0094 {
0095     downloadPayload(entry);
0096 }
0097 
0098 void Installation::downloadPayload(const KNSCore::Entry &entry)
0099 {
0100     if (!entry.isValid()) {
0101         Q_EMIT signalInstallationFailed(i18n("Invalid item."), entry);
0102         return;
0103     }
0104     QUrl source = QUrl(entry.payload());
0105 
0106     if (!source.isValid()) {
0107         qCCritical(KNEWSTUFFCORE) << "The entry doesn't have a payload.";
0108         Q_EMIT signalInstallationFailed(i18n("Download of item failed: no download URL for \"%1\".", entry.name()), entry);
0109         return;
0110     }
0111 
0112     QString fileName(source.fileName());
0113     QTemporaryFile tempFile(QDir::tempPath() + QStringLiteral("/XXXXXX-") + fileName);
0114     tempFile.setAutoRemove(false);
0115     if (!tempFile.open()) {
0116         return; // ERROR
0117     }
0118     QUrl destination = QUrl::fromLocalFile(tempFile.fileName());
0119     qCDebug(KNEWSTUFFCORE) << "Downloading payload" << source << "to" << destination;
0120 #ifdef Q_OS_WIN // can't write to the file if it's open, on Windows
0121     tempFile.close();
0122 #endif
0123 
0124     // FIXME: check for validity
0125     FileCopyJob *job = FileCopyJob::file_copy(source, destination, -1, JobFlag::Overwrite | JobFlag::HideProgressInfo);
0126     connect(job, &KJob::result, this, &Installation::slotPayloadResult);
0127 
0128     entry_jobs[job] = entry;
0129 }
0130 
0131 void Installation::slotPayloadResult(KJob *job)
0132 {
0133     // for some reason this slot is getting called 3 times on one job error
0134     if (entry_jobs.contains(job)) {
0135         Entry entry = entry_jobs[job];
0136         entry_jobs.remove(job);
0137 
0138         if (job->error()) {
0139             const QString errorMessage = i18n("Download of \"%1\" failed, error: %2", entry.name(), job->errorString());
0140             qCWarning(KNEWSTUFFCORE) << errorMessage;
0141             Q_EMIT signalInstallationFailed(errorMessage, entry);
0142         } else {
0143             FileCopyJob *fcjob = static_cast<FileCopyJob *>(job);
0144             qCDebug(KNEWSTUFFCORE) << "Copied to" << fcjob->destUrl();
0145             QMimeDatabase db;
0146             QMimeType mimeType = db.mimeTypeForFile(fcjob->destUrl().toLocalFile());
0147             if (mimeType.inherits(QStringLiteral("text/html")) || mimeType.inherits(QStringLiteral("application/x-php"))) {
0148                 const auto error = i18n("Cannot install '%1' because it points to a web page. Click <a href='%2'>here</a> to finish the installation.",
0149                                         entry.name(),
0150                                         fcjob->srcUrl().toString());
0151                 Q_EMIT signalInstallationFailed(error, entry);
0152                 entry.setStatus(KNSCore::Entry::Invalid);
0153                 Q_EMIT signalEntryChanged(entry);
0154                 return;
0155             }
0156 
0157             Q_EMIT signalPayloadLoaded(fcjob->destUrl());
0158             install(entry, fcjob->destUrl().toLocalFile());
0159         }
0160     }
0161 }
0162 
0163 void KNSCore::Installation::install(KNSCore::Entry entry, const QString &downloadedFile)
0164 {
0165     qCWarning(KNEWSTUFFCORE) << "Install:" << entry.name() << "from" << downloadedFile;
0166     Q_ASSERT(QFileInfo::exists(downloadedFile));
0167 
0168     if (entry.payload().isEmpty()) {
0169         qCDebug(KNEWSTUFFCORE) << "No payload associated with:" << entry.name();
0170         return;
0171     }
0172 
0173     // TODO Add async checksum verification
0174 
0175     QString targetPath = targetInstallationPath();
0176     QStringList installedFiles = installDownloadedFileAndUncompress(entry, downloadedFile, targetPath);
0177 
0178     if (uncompressionSetting() != UseKPackageUncompression) {
0179         if (installedFiles.isEmpty()) {
0180             if (entry.status() == KNSCore::Entry::Installing) {
0181                 entry.setStatus(KNSCore::Entry::Downloadable);
0182             } else if (entry.status() == KNSCore::Entry::Updating) {
0183                 entry.setStatus(KNSCore::Entry::Updateable);
0184             }
0185             Q_EMIT signalEntryChanged(entry);
0186             Q_EMIT signalInstallationFailed(i18n("Could not install \"%1\": file not found.", entry.name()), entry);
0187             return;
0188         }
0189 
0190         entry.setInstalledFiles(installedFiles);
0191 
0192         auto installationFinished = [this, entry]() {
0193             Entry newentry = entry;
0194             if (!newentry.updateVersion().isEmpty()) {
0195                 newentry.setVersion(newentry.updateVersion());
0196             }
0197             if (newentry.updateReleaseDate().isValid()) {
0198                 newentry.setReleaseDate(newentry.updateReleaseDate());
0199             }
0200             newentry.setStatus(KNSCore::Entry::Installed);
0201             Q_EMIT signalEntryChanged(newentry);
0202             Q_EMIT signalInstallationFinished(newentry);
0203         };
0204         if (!postInstallationCommand.isEmpty()) {
0205             QString scriptArgPath = !installedFiles.isEmpty() ? installedFiles.first() : targetPath;
0206             if (scriptArgPath.endsWith(QLatin1Char('*'))) {
0207                 scriptArgPath = scriptArgPath.left(scriptArgPath.lastIndexOf(QLatin1Char('*')));
0208             }
0209             QProcess *p = runPostInstallationCommand(scriptArgPath, entry);
0210             connect(p, &QProcess::finished, this, [entry, installationFinished, this](int exitCode) {
0211                 if (exitCode) {
0212                     Entry newEntry = entry;
0213                     newEntry.setStatus(KNSCore::Entry::Invalid);
0214                     Q_EMIT signalEntryChanged(newEntry);
0215                 } else {
0216                     installationFinished();
0217                 }
0218             });
0219         } else {
0220             installationFinished();
0221         }
0222     }
0223 }
0224 
0225 QString Installation::targetInstallationPath() const
0226 {
0227     // installdir is the target directory
0228     QString installdir;
0229 
0230     const bool userScope = true;
0231     // installpath also contains the file name if it's a single file, otherwise equal to installdir
0232     int pathcounter = 0;
0233     // wallpaper is already managed in the case of !xdgTargetDirectory.isEmpty()
0234     if (!standardResourceDirectory.isEmpty() && standardResourceDirectory != QLatin1String("wallpaper")) {
0235         QStandardPaths::StandardLocation location = QStandardPaths::TempLocation;
0236         // crude translation KStandardDirs names -> QStandardPaths enum
0237         if (standardResourceDirectory == QLatin1String("tmp")) {
0238             location = QStandardPaths::TempLocation;
0239         } else if (standardResourceDirectory == QLatin1String("config")) {
0240             location = QStandardPaths::ConfigLocation;
0241         }
0242 
0243         if (userScope) {
0244             installdir = QStandardPaths::writableLocation(location);
0245         } else { // system scope
0246             installdir = QStandardPaths::standardLocations(location).constLast();
0247         }
0248         pathcounter++;
0249     }
0250     if (!targetDirectory.isEmpty() && targetDirectory != QLatin1String("/")) {
0251         if (userScope) {
0252             installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + targetDirectory + QLatin1Char('/');
0253         } else { // system scope
0254             installdir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, targetDirectory, QStandardPaths::LocateDirectory) + QLatin1Char('/');
0255         }
0256         pathcounter++;
0257     }
0258     if (!xdgTargetDirectory.isEmpty() && xdgTargetDirectory != QLatin1String("/")) {
0259         installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + xdgTargetDirectory + QLatin1Char('/');
0260         pathcounter++;
0261     }
0262     if (!installPath.isEmpty()) {
0263 #if defined(Q_OS_WIN)
0264         WCHAR wPath[MAX_PATH + 1];
0265         if (SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, wPath) == S_OK) {
0266             installdir = QString::fromUtf16((const char16_t *)wPath) + QLatin1Char('/') + installPath + QLatin1Char('/');
0267         } else {
0268             installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/');
0269         }
0270 #else
0271         installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/');
0272 #endif
0273         pathcounter++;
0274     }
0275     if (!absoluteInstallPath.isEmpty()) {
0276         installdir = absoluteInstallPath + QLatin1Char('/');
0277         pathcounter++;
0278     }
0279 
0280     if (pathcounter != 1) {
0281         qCCritical(KNEWSTUFFCORE) << "Wrong number of installation directories given.";
0282         return QString();
0283     }
0284 
0285     qCDebug(KNEWSTUFFCORE) << "installdir: " << installdir;
0286 
0287     // create the dir if it doesn't exist (QStandardPaths doesn't create it, unlike KStandardDirs!)
0288     QDir().mkpath(installdir);
0289 
0290     return installdir;
0291 }
0292 
0293 QStringList Installation::installDownloadedFileAndUncompress(const KNSCore::Entry &entry, const QString &payloadfile, const QString installdir)
0294 {
0295     // Collect all files that were installed
0296     QStringList installedFiles;
0297     bool isarchive = true;
0298     UncompressionOptions uncompressionOpt = uncompressionSetting();
0299 
0300     // respect the uncompress flag in the knsrc
0301     if (uncompressionOpt == UseKPackageUncompression) {
0302         qCDebug(KNEWSTUFFCORE) << "Using KPackage for installation";
0303         auto resetEntryStatus = [this, entry]() {
0304             KNSCore::Entry changedEntry(entry);
0305             if (changedEntry.status() == KNSCore::Entry::Installing || changedEntry.status() == KNSCore::Entry::Installed) {
0306                 changedEntry.setStatus(KNSCore::Entry::Downloadable);
0307             } else if (changedEntry.status() == KNSCore::Entry::Updating) {
0308                 changedEntry.setStatus(KNSCore::Entry::Updateable);
0309             }
0310             Q_EMIT signalEntryChanged(changedEntry);
0311         };
0312 
0313         qCDebug(KNEWSTUFFCORE) << "About to attempt to install" << payloadfile << "as" << kpackageStructure;
0314         auto job = KPackage::PackageJob::install(kpackageStructure, payloadfile);
0315         connect(job, &KPackage::PackageJob::finished, this, [this, entry, payloadfile, resetEntryStatus, job]() {
0316             if (job->error() == KJob::NoError) {
0317                 Entry newentry = entry;
0318                 newentry.setInstalledFiles(QStringList{job->package().path()});
0319                 // update version and release date to the new ones
0320                 if (newentry.status() == KNSCore::Entry::Updating) {
0321                     if (!newentry.updateVersion().isEmpty()) {
0322                         newentry.setVersion(newentry.updateVersion());
0323                     }
0324                     if (newentry.updateReleaseDate().isValid()) {
0325                         newentry.setReleaseDate(newentry.updateReleaseDate());
0326                     }
0327                 }
0328                 newentry.setStatus(KNSCore::Entry::Installed);
0329                 // We can remove the downloaded file, because we don't save its location and don't need it to uninstall the entry
0330                 QFile::remove(payloadfile);
0331                 Q_EMIT signalEntryChanged(newentry);
0332                 Q_EMIT signalInstallationFinished(newentry);
0333                 qCDebug(KNEWSTUFFCORE) << "Install job finished with no error and we now have files" << job->package().path();
0334             } else {
0335                 if (job->error() == KPackage::PackageJob::JobError::NewerVersionAlreadyInstalledError) {
0336                     Entry newentry = entry;
0337                     newentry.setStatus(KNSCore::Entry::Installed);
0338                     newentry.setInstalledFiles(QStringList{job->package().path()});
0339                     // update version and release date to the new ones
0340                     if (!newentry.updateVersion().isEmpty()) {
0341                         newentry.setVersion(newentry.updateVersion());
0342                     }
0343                     if (newentry.updateReleaseDate().isValid()) {
0344                         newentry.setReleaseDate(newentry.updateReleaseDate());
0345                     }
0346                     Q_EMIT signalEntryChanged(newentry);
0347                     Q_EMIT signalInstallationFinished(newentry);
0348                     qCDebug(KNEWSTUFFCORE) << "Install job finished telling us this item was already installed with this version, so... let's "
0349                                               "just make a small fib and say we totally installed that, honest, and we now have files"
0350                                            << job->package().path();
0351                 } else {
0352                     Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2", payloadfile, job->errorText()), entry);
0353                     resetEntryStatus();
0354                     qCDebug(KNEWSTUFFCORE) << "Install job finished with error state" << job->error() << "and description" << job->error();
0355                 }
0356             }
0357         });
0358     } else {
0359         if (uncompressionOpt == AlwaysUncompress || uncompressionOpt == UncompressIntoSubdirIfArchive || uncompressionOpt == UncompressIfArchive
0360             || uncompressionOpt == UncompressIntoSubdir) {
0361             // this is weird but a decompression is not a single name, so take the path instead
0362             QMimeDatabase db;
0363             QMimeType mimeType = db.mimeTypeForFile(payloadfile);
0364             qCDebug(KNEWSTUFFCORE) << "Postinstallation: uncompress the file";
0365 
0366             // FIXME: check for overwriting, malicious archive entries (../foo) etc.
0367             // FIXME: KArchive should provide "safe mode" for this!
0368             QScopedPointer<KArchive> archive;
0369 
0370             if (mimeType.inherits(QStringLiteral("application/zip"))) {
0371                 archive.reset(new KZip(payloadfile));
0372                 // clang-format off
0373             } else if (mimeType.inherits(QStringLiteral("application/tar"))
0374                     || mimeType.inherits(QStringLiteral("application/x-tar")) // BUG 450662
0375                     || mimeType.inherits(QStringLiteral("application/x-gzip"))
0376                     || mimeType.inherits(QStringLiteral("application/x-bzip"))
0377                     || mimeType.inherits(QStringLiteral("application/x-lzma"))
0378                     || mimeType.inherits(QStringLiteral("application/x-xz"))
0379                     || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar"))
0380                     || mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) {
0381                 // clang-format on
0382                 archive.reset(new KTar(payloadfile));
0383             } else {
0384                 qCCritical(KNEWSTUFFCORE) << "Could not determine type of archive file" << payloadfile;
0385                 if (uncompressionOpt == AlwaysUncompress) {
0386                     Q_EMIT signalInstallationError(i18n("Could not determine the type of archive of the downloaded file %1", payloadfile), entry);
0387                     return QStringList();
0388                 }
0389                 isarchive = false;
0390             }
0391 
0392             if (isarchive) {
0393                 bool success = archive->open(QIODevice::ReadOnly);
0394                 if (!success) {
0395                     qCCritical(KNEWSTUFFCORE) << "Cannot open archive file" << payloadfile;
0396                     if (uncompressionOpt == AlwaysUncompress) {
0397                         Q_EMIT signalInstallationError(
0398                             i18n("Failed to open the archive file %1. The reported error was: %2", payloadfile, archive->errorString()),
0399                             entry);
0400                         return QStringList();
0401                     }
0402                     // otherwise, just copy the file
0403                     isarchive = false;
0404                 }
0405 
0406                 if (isarchive) {
0407                     const KArchiveDirectory *dir = archive->directory();
0408                     // if there is more than an item in the file, and we are requested to do so
0409                     // put contents in a subdirectory with the same name as the file
0410                     QString installpath;
0411                     const bool isSubdir =
0412                         (uncompressionOpt == UncompressIntoSubdir || uncompressionOpt == UncompressIntoSubdirIfArchive) && dir->entries().count() > 1;
0413                     if (isSubdir) {
0414                         installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName();
0415                     } else {
0416                         installpath = installdir;
0417                     }
0418 
0419                     if (dir->copyTo(installpath)) {
0420                         // If we extracted the subdir we want to save it using the /* notation like we would when using the "archive" option
0421                         // Also if we use an (un)install command we only call it once with the folder as argument and not for each file
0422                         if (isSubdir) {
0423                             installedFiles << QDir(installpath).absolutePath() + QLatin1String("/*");
0424                         } else {
0425                             installedFiles << archiveEntries(installpath, dir);
0426                         }
0427                     } else {
0428                         qCWarning(KNEWSTUFFCORE) << "could not install" << entry.name() << "to" << installpath;
0429                     }
0430 
0431                     archive->close();
0432                     QFile::remove(payloadfile);
0433                 }
0434             }
0435         }
0436 
0437         qCDebug(KNEWSTUFFCORE) << "isarchive:" << isarchive;
0438 
0439         // some wallpapers are compressed, some aren't
0440         if ((!isarchive && standardResourceDirectory == QLatin1String("wallpaper"))
0441             || (uncompressionOpt == NeverUncompress || (uncompressionOpt == UncompressIfArchive && !isarchive)
0442                 || (uncompressionOpt == UncompressIntoSubdirIfArchive && !isarchive))) {
0443             // no decompress but move to target
0444 
0445             /// @todo when using KIO::get the http header can be accessed and it contains a real file name.
0446             // FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names
0447             QUrl source = QUrl(entry.payload());
0448             qCDebug(KNEWSTUFFCORE) << "installing non-archive from" << source;
0449             const QString installpath = QDir(installdir).filePath(source.fileName());
0450 
0451             qCDebug(KNEWSTUFFCORE) << "Install to file" << installpath;
0452             // FIXME: copy goes here (including overwrite checking)
0453             // FIXME: what must be done now is to update the cache *again*
0454             //        in order to set the new payload filename (on root tag only)
0455             //        - this might or might not need to take uncompression into account
0456             // FIXME: for updates, we might need to force an overwrite (that is, deleting before)
0457             QFile file(payloadfile);
0458             bool success = true;
0459             const bool update = ((entry.status() == KNSCore::Entry::Updateable) || (entry.status() == KNSCore::Entry::Updating));
0460 
0461             if (QFile::exists(installpath) && QDir::tempPath() != installdir) {
0462                 if (!update) {
0463                     Question question(Question::YesNoQuestion);
0464                     question.setEntry(entry);
0465                     question.setQuestion(i18n("This file already exists on disk (possibly due to an earlier failed download attempt). Continuing means "
0466                                               "overwriting it. Do you wish to overwrite the existing file?")
0467                                          + QStringLiteral("\n'") + installpath + QLatin1Char('\''));
0468                     question.setTitle(i18n("Overwrite File"));
0469                     if (question.ask() != Question::YesResponse) {
0470                         return QStringList();
0471                     }
0472                 }
0473                 success = QFile::remove(installpath);
0474             }
0475             if (success) {
0476                 // remove in case it's already present and in a temporary directory, so we get to actually use the path again
0477                 if (installpath.startsWith(QDir::tempPath())) {
0478                     QFile::remove(installpath);
0479                 }
0480                 success = file.rename(installpath);
0481                 qCWarning(KNEWSTUFFCORE) << "move:" << file.fileName() << "to" << installpath;
0482                 if (!success) {
0483                     qCWarning(KNEWSTUFFCORE) << file.errorString();
0484                 }
0485             }
0486             if (!success) {
0487                 Q_EMIT signalInstallationError(i18n("Unable to move the file %1 to the intended destination %2", payloadfile, installpath), entry);
0488                 qCCritical(KNEWSTUFFCORE) << "Cannot move file" << payloadfile << "to destination" << installpath;
0489                 return QStringList();
0490             }
0491             installedFiles << installpath;
0492         }
0493     }
0494 
0495     return installedFiles;
0496 }
0497 
0498 QProcess *Installation::runPostInstallationCommand(const QString &installPath, const KNSCore::Entry &entry)
0499 {
0500     QString command(postInstallationCommand);
0501     QString fileArg(KShell::quoteArg(installPath));
0502     command.replace(QLatin1String("%f"), fileArg);
0503 
0504     qCDebug(KNEWSTUFFCORE) << "Run command:" << command;
0505 
0506     QProcess *ret = new QProcess(this);
0507     auto onProcessFinished = [this, command, ret, entry](int exitcode, QProcess::ExitStatus status) {
0508         const QString output{QString::fromLocal8Bit(ret->readAllStandardError())};
0509         if (status == QProcess::CrashExit) {
0510             QString errorMessage = i18n("The installation failed while attempting to run the command:\n%1\n\nThe returned output was:\n%2", command, output);
0511             Q_EMIT signalInstallationError(errorMessage, entry);
0512             qCCritical(KNEWSTUFFCORE) << "Process crashed with command:" << command;
0513         } else if (exitcode) {
0514             // 130 means Ctrl+C as an exit code this is interpreted by KNewStuff as cancel operation
0515             // and no error will be displayed to the user, BUG: 436355
0516             if (exitcode == 130) {
0517                 qCCritical(KNEWSTUFFCORE) << "Command" << command << "failed was aborted by the user";
0518                 Q_EMIT signalInstallationFinished(entry);
0519             } else {
0520                 Q_EMIT signalInstallationError(
0521                     i18n("The installation failed with code %1 while attempting to run the command:\n%2\n\nThe returned output was:\n%3",
0522                          exitcode,
0523                          command,
0524                          output),
0525                     entry);
0526                 qCCritical(KNEWSTUFFCORE) << "Command" << command << "failed with code" << exitcode;
0527             }
0528         }
0529         sender()->deleteLater();
0530     };
0531     connect(ret, &QProcess::finished, this, onProcessFinished);
0532 
0533     QStringList args = KShell::splitArgs(command);
0534     ret->setProgram(args.takeFirst());
0535     ret->setArguments(args);
0536     ret->start();
0537     return ret;
0538 }
0539 
0540 void Installation::uninstall(Entry entry)
0541 {
0542     const auto deleteFilesAndMarkAsUninstalled = [entry, this]() {
0543         bool deletionSuccessful = true;
0544         const auto lst = entry.installedFiles();
0545         for (const QString &file : lst) {
0546             // This is used to delete the download location if there are no more entries
0547             QFileInfo info(file);
0548             if (info.isDir()) {
0549                 QDir().rmdir(file);
0550             } else if (file.endsWith(QLatin1String("/*"))) {
0551                 QDir dir(file.left(file.size() - 2));
0552                 bool worked = dir.removeRecursively();
0553                 if (!worked) {
0554                     qCWarning(KNEWSTUFFCORE) << "Couldn't remove" << dir.path();
0555                     continue;
0556                 }
0557             } else {
0558                 if (info.exists() || info.isSymLink()) {
0559                     bool worked = QFile::remove(file);
0560                     if (!worked) {
0561                         qWarning() << "unable to delete file " << file;
0562                         Q_EMIT signalInstallationFailed(
0563                             i18n("The removal of %1 failed, as the installed file %2 could not be automatically removed. You can attempt to manually delete "
0564                                  "this file, if you believe this is an error.",
0565                                  entry.name(),
0566                                  file),
0567                             entry);
0568                         // Assume that the uninstallation has failed, and reset the entry to an installed state
0569                         deletionSuccessful = false;
0570                         break;
0571                     }
0572                 } else {
0573                     qWarning() << "unable to delete file " << file << ". file does not exist.";
0574                 }
0575             }
0576         }
0577         Entry newEntry = entry;
0578         if (deletionSuccessful) {
0579             newEntry.setEntryDeleted();
0580         } else {
0581             newEntry.setStatus(KNSCore::Entry::Installed);
0582         }
0583 
0584         Q_EMIT signalEntryChanged(newEntry);
0585     };
0586 
0587     if (uncompressionSetting() == UseKPackageUncompression) {
0588         const auto lst = entry.installedFiles();
0589         if (lst.length() == 1) {
0590             const QString installedFile{lst.first()};
0591 
0592             KJob *job = KPackage::PackageJob::uninstall(kpackageStructure, installedFile);
0593             connect(job, &KJob::result, this, [this, installedFile, entry, job]() {
0594                 Entry newEntry = entry;
0595                 if (job->error() == KJob::NoError) {
0596                     newEntry.setEntryDeleted();
0597                     Q_EMIT signalEntryChanged(newEntry);
0598                 } else {
0599                     Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2", installedFile, job->errorText()), entry);
0600                 }
0601             });
0602         }
0603         deleteFilesAndMarkAsUninstalled();
0604     } else {
0605         const auto lst = entry.installedFiles();
0606         // If there is an uninstall script, make sure it runs without errors
0607         if (!uninstallCommand.isEmpty()) {
0608             bool validFileExisted = false;
0609             for (const QString &file : lst) {
0610                 QString filePath = file;
0611                 bool validFile = QFileInfo::exists(filePath);
0612                 // If we have uncompressed a subdir we write <path>/* in the config, but when calling a script
0613                 // we want to convert this to a normal path
0614                 if (!validFile && file.endsWith(QLatin1Char('*'))) {
0615                     filePath = filePath.left(filePath.lastIndexOf(QLatin1Char('*')));
0616                     validFile = QFileInfo::exists(filePath);
0617                 }
0618                 if (validFile) {
0619                     validFileExisted = true;
0620                     QString fileArg(KShell::quoteArg(filePath));
0621                     QString command(uninstallCommand);
0622                     command.replace(QLatin1String("%f"), fileArg);
0623 
0624                     QStringList args = KShell::splitArgs(command);
0625                     const QString program = args.takeFirst();
0626                     QProcess *process = new QProcess(this);
0627                     process->start(program, args);
0628                     auto onProcessFinished = [this, command, process, entry, deleteFilesAndMarkAsUninstalled](int, QProcess::ExitStatus status) {
0629                         if (status == QProcess::CrashExit) {
0630                             const QString processOutput = QString::fromLocal8Bit(process->readAllStandardError());
0631                             const QString err = i18n(
0632                                 "The uninstallation process failed to successfully run the command %1\n"
0633                                 "The output of was: \n%2\n"
0634                                 "If you think this is incorrect, you can continue or cancel the uninstallation process",
0635                                 KShell::quoteArg(command),
0636                                 processOutput);
0637                             Q_EMIT signalInstallationError(err, entry);
0638                             // Ask the user if he wants to continue, even though the script failed
0639                             Question question(Question::ContinueCancelQuestion);
0640                             question.setEntry(entry);
0641                             question.setQuestion(err);
0642                             Question::Response response = question.ask();
0643                             if (response == Question::CancelResponse) {
0644                                 // Use can delete files manually
0645                                 Entry newEntry = entry;
0646                                 newEntry.setStatus(KNSCore::Entry::Installed);
0647                                 Q_EMIT signalEntryChanged(newEntry);
0648                                 return;
0649                             }
0650                         } else {
0651                             qCDebug(KNEWSTUFFCORE) << "Command executed successfully:" << command;
0652                         }
0653                         deleteFilesAndMarkAsUninstalled();
0654                     };
0655                     connect(process, &QProcess::finished, this, onProcessFinished);
0656                 }
0657             }
0658             // If the entry got deleted, but the RemoveDeadEntries option was not selected this case can happen
0659             if (!validFileExisted) {
0660                 deleteFilesAndMarkAsUninstalled();
0661             }
0662         } else {
0663             deleteFilesAndMarkAsUninstalled();
0664         }
0665     }
0666 }
0667 
0668 Installation::UncompressionOptions Installation::uncompressionSetting() const
0669 {
0670     return uncompressSetting;
0671 }
0672 
0673 QStringList Installation::archiveEntries(const QString &path, const KArchiveDirectory *dir)
0674 {
0675     QStringList files;
0676     const auto lst = dir->entries();
0677     for (const QString &entry : lst) {
0678         const auto currentEntry = dir->entry(entry);
0679 
0680         const QString childPath = QDir(path).filePath(entry);
0681         if (currentEntry->isFile()) {
0682             files << childPath;
0683         } else if (currentEntry->isDirectory()) {
0684             files << childPath + QStringLiteral("/*");
0685         }
0686     }
0687     return files;
0688 }
0689 
0690 #include "moc_installation_p.cpp"