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"