File indexing completed on 2024-04-28 15:28:57
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.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 "qmimedatabase.h" 0020 #include <KRandom> 0021 #include <KShell> 0022 #include <KTar> 0023 #include <KZip> 0024 0025 #include "jobs/kpackagejob.h" 0026 #include <KPackage/Package> 0027 #include <KPackage/PackageLoader> 0028 #include <KPackage/PackageStructure> 0029 0030 #include <KLocalizedString> 0031 #include <knewstuffcore_debug.h> 0032 #include <qstandardpaths.h> 0033 0034 #include "jobs/filecopyjob.h" 0035 #include "question.h" 0036 #ifdef Q_OS_WIN 0037 #include <shlobj.h> 0038 #include <windows.h> 0039 #endif 0040 0041 using namespace KNSCore; 0042 0043 Installation::Installation(QObject *parent) 0044 : QObject(parent) 0045 { 0046 // TODO KF6 Make these real properties, when we can refactor this and add a proper dptr 0047 setProperty("kpackageType", QLatin1String("")); 0048 setProperty("uncompressSetting", UncompressionOptions::NeverUncompress); 0049 } 0050 0051 bool Installation::readConfig(const KConfigGroup &group) 0052 { 0053 // FIXME: add support for several categories later on 0054 // FIXME: read out only when actually installing as a performance improvement? 0055 uncompression = group.readEntry("Uncompress", QStringLiteral("never")); 0056 UncompressionOptions opt; 0057 // support old value of true as equivalent of always 0058 if (uncompression == QLatin1String("true")) { 0059 uncompression = QStringLiteral("always"); 0060 } 0061 if (uncompression == QLatin1String("always")) { 0062 opt = AlwaysUncompress; 0063 } else if (uncompression == QLatin1String("archive")) { 0064 opt = UncompressIfArchive; 0065 } else if (uncompression == QLatin1String("subdir")) { 0066 opt = UncompressIntoSubdir; 0067 } else if (uncompression == QLatin1String("kpackage")) { 0068 opt = UseKPackageUncompression; 0069 } else if (uncompression == QLatin1String("subdir-archive")) { 0070 opt = UncompressIntoSubdirIfArchive; 0071 } else if (uncompression == QLatin1String("never")) { 0072 opt = NeverUncompress; 0073 } else { 0074 qCCritical(KNEWSTUFFCORE) << "invalid Uncompress setting chosen, must be one of: subdir, always, archive, never, or kpackage"; 0075 return false; 0076 } 0077 setProperty("uncompressSetting", opt); 0078 setProperty("kpackageType", group.readEntry("KPackageType")); 0079 postInstallationCommand = group.readEntry("InstallationCommand"); 0080 uninstallCommand = group.readEntry("UninstallCommand"); 0081 standardResourceDirectory = group.readEntry("StandardResource"); 0082 targetDirectory = group.readEntry("TargetDir"); 0083 xdgTargetDirectory = group.readEntry("XdgTargetDir"); 0084 0085 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 77) 0086 // Provide some compatibility 0087 if (standardResourceDirectory == QLatin1String("wallpaper")) { 0088 xdgTargetDirectory = QStringLiteral("wallpapers"); 0089 } 0090 0091 // also, ensure wallpapers are decompressed into subdirectories 0092 // this ensures that wallpapers with multiple resolutions continue to function 0093 // as expected 0094 if (xdgTargetDirectory == QLatin1String("wallpapers")) { 0095 uncompression = QStringLiteral("subdir"); 0096 } 0097 0098 // A touch of special treatment for the various old kpackage based knsrc files, so they work 0099 // with the new, internal stuff. The result unfortunately is that all entries marked as 0100 // installed in knewstuff no longer will be, but since it never worked right anyway... we'll 0101 // simply have to live with that. 0102 // clang-format off 0103 if (postInstallationCommand.startsWith(QLatin1String("kpackagetool5 -t")) && 0104 postInstallationCommand.endsWith(QLatin1String("-i %f")) && 0105 uninstallCommand.startsWith(QLatin1String("kpackagetool5 -t")) && 0106 uninstallCommand.endsWith(QLatin1String("-r %f"))) { 0107 uncompression = QStringLiteral("kpackage"); 0108 postInstallationCommand = QLatin1String(""); 0109 // Not clearing uninstallCommand, as this is used for the fallback situation 0110 setProperty("kpackageType", uninstallCommand.mid(17, uninstallCommand.length() - 17 - 6)); 0111 qCWarning(KNEWSTUFFCORE) << "Your configuration file uses an old version of the kpackage support, and should be converted. Please report this to the author of the software you are currently using. The package type, we assume, is" << property("kpackageType").toString(); 0112 } else if (postInstallationCommand.startsWith(QLatin1String("kpackagetool5 --type")) && 0113 postInstallationCommand.endsWith(QLatin1String("--install %f")) && 0114 uninstallCommand.startsWith(QLatin1String("kpackagetool5 --type")) && 0115 uninstallCommand.endsWith(QLatin1String("--remove %f"))) { 0116 uncompression = QStringLiteral("kpackage"); 0117 postInstallationCommand = QLatin1String(""); 0118 // Not clearing uninstallCommand, as this is used for the fallback situation 0119 setProperty("kpackageType", uninstallCommand.mid(21, uninstallCommand.length() - 21 - 12)); 0120 qCWarning(KNEWSTUFFCORE) << "Your configuration file uses an old version of the kpackage support, and should be converted. Please report this to the author of the software you are currently using. The package type, we assume, is" << property("kpackageType").toString(); 0121 } 0122 // clang-format on 0123 #endif 0124 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 79) 0125 customName = group.readEntry("CustomName", false); 0126 if (customName) { 0127 qWarning(KNEWSTUFFCORE) << "The CustomName property is deprecated and will be removed in KF6"; 0128 } 0129 QString scopeString = group.readEntry("Scope"); 0130 if (!scopeString.isEmpty()) { 0131 qWarning(KNEWSTUFFCORE) << "Setting the scope is deprecated, it will default to user"; 0132 if (scopeString == QLatin1String("user")) { 0133 scope = ScopeUser; 0134 } else if (scopeString == QLatin1String("system")) { 0135 scope = ScopeSystem; 0136 } else { 0137 qCCritical(KNEWSTUFFCORE) << QStringLiteral("The scope '") + scopeString + QStringLiteral("' is unknown."); 0138 return false; 0139 } 0140 0141 if (scope == ScopeSystem) { 0142 if (!installPath.isEmpty()) { 0143 qCCritical(KNEWSTUFFCORE) << "System installation cannot be mixed with InstallPath."; 0144 return false; 0145 } 0146 } 0147 } 0148 0149 QString checksumpolicy = group.readEntry("ChecksumPolicy"); 0150 if (!checksumpolicy.isEmpty()) { 0151 qWarning(KNEWSTUFFCORE) << "The ChecksumPolicy feature is defunct"; 0152 if (checksumpolicy == QLatin1String("never")) { 0153 checksumPolicy = Installation::CheckNever; 0154 } else if (checksumpolicy == QLatin1String("ifpossible")) { 0155 checksumPolicy = Installation::CheckIfPossible; 0156 } else if (checksumpolicy == QLatin1String("always")) { 0157 checksumPolicy = Installation::CheckAlways; 0158 } else { 0159 qCCritical(KNEWSTUFFCORE) << QStringLiteral("The checksum policy '") + checksumpolicy + QStringLiteral("' is unknown."); 0160 return false; 0161 } 0162 } 0163 0164 QString signaturepolicy = group.readEntry("SignaturePolicy"); 0165 if (!signaturepolicy.isEmpty()) { 0166 qWarning(KNEWSTUFFCORE) << "The SignaturePolicy feature is defunct"; 0167 if (signaturepolicy == QLatin1String("never")) { 0168 signaturePolicy = Installation::CheckNever; 0169 } else if (signaturepolicy == QLatin1String("ifpossible")) { 0170 signaturePolicy = Installation::CheckIfPossible; 0171 } else if (signaturepolicy == QLatin1String("always")) { 0172 signaturePolicy = Installation::CheckAlways; 0173 } else { 0174 qCCritical(KNEWSTUFFCORE) << QStringLiteral("The signature policy '") + signaturepolicy + QStringLiteral("' is unknown."); 0175 return false; 0176 } 0177 } 0178 acceptHtml = group.readEntry("AcceptHtmlDownloads", false); 0179 if (acceptHtml) { 0180 qWarning(KNEWSTUFFCORE) << "The AcceptHtmlDownload property is deprecated and will default to false. If there" 0181 "is a HTML download link the user will be prompted if the installation should proceed"; 0182 } 0183 #endif 0184 0185 installPath = group.readEntry("InstallPath"); 0186 absoluteInstallPath = group.readEntry("AbsoluteInstallPath"); 0187 0188 if (standardResourceDirectory.isEmpty() && targetDirectory.isEmpty() && xdgTargetDirectory.isEmpty() && installPath.isEmpty() 0189 && absoluteInstallPath.isEmpty()) { 0190 qCCritical(KNEWSTUFFCORE) << "No installation target set"; 0191 return false; 0192 } 0193 return true; 0194 } 0195 0196 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 71) 0197 bool Installation::isRemote() const 0198 { 0199 return false; 0200 } 0201 #endif 0202 0203 void Installation::install(const EntryInternal &entry) 0204 { 0205 downloadPayload(entry); 0206 } 0207 0208 void Installation::downloadPayload(const KNSCore::EntryInternal &entry) 0209 { 0210 if (!entry.isValid()) { 0211 Q_EMIT signalInstallationFailed(i18n("Invalid item.")); 0212 return; 0213 } 0214 QUrl source = QUrl(entry.payload()); 0215 0216 if (!source.isValid()) { 0217 qCCritical(KNEWSTUFFCORE) << "The entry doesn't have a payload."; 0218 Q_EMIT signalInstallationFailed(i18n("Download of item failed: no download URL for \"%1\".", entry.name())); 0219 return; 0220 } 0221 0222 QString fileName(source.fileName()); 0223 QTemporaryFile tempFile(QDir::tempPath() + QStringLiteral("/XXXXXX-") + fileName); 0224 tempFile.setAutoRemove(false); 0225 if (!tempFile.open()) { 0226 return; // ERROR 0227 } 0228 QUrl destination = QUrl::fromLocalFile(tempFile.fileName()); 0229 qCDebug(KNEWSTUFFCORE) << "Downloading payload" << source << "to" << destination; 0230 #ifdef Q_OS_WIN // can't write to the file if it's open, on Windows 0231 tempFile.close(); 0232 #endif 0233 0234 // FIXME: check for validity 0235 FileCopyJob *job = FileCopyJob::file_copy(source, destination, -1, JobFlag::Overwrite | JobFlag::HideProgressInfo); 0236 connect(job, &KJob::result, this, &Installation::slotPayloadResult); 0237 0238 entry_jobs[job] = entry; 0239 } 0240 0241 void Installation::slotPayloadResult(KJob *job) 0242 { 0243 // for some reason this slot is getting called 3 times on one job error 0244 if (entry_jobs.contains(job)) { 0245 EntryInternal entry = entry_jobs[job]; 0246 entry_jobs.remove(job); 0247 0248 if (job->error()) { 0249 const QString errorMessage = i18n("Download of \"%1\" failed, error: %2", entry.name(), job->errorString()); 0250 qCWarning(KNEWSTUFFCORE) << errorMessage; 0251 Q_EMIT signalInstallationFailed(errorMessage); 0252 } else { 0253 FileCopyJob *fcjob = static_cast<FileCopyJob *>(job); 0254 qCDebug(KNEWSTUFFCORE) << "Copied to" << fcjob->destUrl(); 0255 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 79) 0256 // check if the app likes html files - disabled by default as too many bad links have been submitted to opendesktop.org 0257 if (!acceptHtml) { 0258 #endif 0259 QMimeDatabase db; 0260 QMimeType mimeType = db.mimeTypeForFile(fcjob->destUrl().toLocalFile()); 0261 if (mimeType.inherits(QStringLiteral("text/html")) || mimeType.inherits(QStringLiteral("application/x-php"))) { 0262 const auto error = i18n("Cannot install '%1' because it points to a web page. Click <a href='%2'>here</a> to finish the installation.", 0263 entry.name(), 0264 fcjob->srcUrl().toString()); 0265 Q_EMIT signalInstallationFailed(error); 0266 entry.setStatus(KNS3::Entry::Invalid); 0267 Q_EMIT signalEntryChanged(entry); 0268 return; 0269 } 0270 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 79) 0271 } 0272 #endif 0273 0274 Q_EMIT signalPayloadLoaded(fcjob->destUrl()); 0275 install(entry, fcjob->destUrl().toLocalFile()); 0276 } 0277 } 0278 } 0279 0280 void KNSCore::Installation::install(KNSCore::EntryInternal entry, const QString &downloadedFile) 0281 { 0282 qCDebug(KNEWSTUFFCORE) << "Install:" << entry.name() << "from" << downloadedFile; 0283 Q_ASSERT(QFileInfo::exists(downloadedFile)); 0284 0285 if (entry.payload().isEmpty()) { 0286 qCDebug(KNEWSTUFFCORE) << "No payload associated with:" << entry.name(); 0287 return; 0288 } 0289 0290 // TODO Add async checksum verification 0291 0292 QString targetPath = targetInstallationPath(); 0293 QStringList installedFiles = installDownloadedFileAndUncompress(entry, downloadedFile, targetPath); 0294 0295 if (uncompressionSetting() != UseKPackageUncompression) { 0296 if (installedFiles.isEmpty()) { 0297 if (entry.status() == KNS3::Entry::Installing) { 0298 entry.setStatus(KNS3::Entry::Downloadable); 0299 } else if (entry.status() == KNS3::Entry::Updating) { 0300 entry.setStatus(KNS3::Entry::Updateable); 0301 } 0302 Q_EMIT signalEntryChanged(entry); 0303 Q_EMIT signalInstallationFailed(i18n("Could not install \"%1\": file not found.", entry.name())); 0304 return; 0305 } 0306 0307 entry.setInstalledFiles(installedFiles); 0308 0309 auto installationFinished = [this, entry]() { 0310 EntryInternal newentry = entry; 0311 if (!newentry.updateVersion().isEmpty()) { 0312 newentry.setVersion(newentry.updateVersion()); 0313 } 0314 if (newentry.updateReleaseDate().isValid()) { 0315 newentry.setReleaseDate(newentry.updateReleaseDate()); 0316 } 0317 newentry.setStatus(KNS3::Entry::Installed); 0318 Q_EMIT signalEntryChanged(newentry); 0319 Q_EMIT signalInstallationFinished(); 0320 }; 0321 if (!postInstallationCommand.isEmpty()) { 0322 QString scriptArgPath = !installedFiles.isEmpty() ? installedFiles.first() : targetPath; 0323 if (scriptArgPath.endsWith(QLatin1Char('*'))) { 0324 scriptArgPath = scriptArgPath.left(scriptArgPath.lastIndexOf(QLatin1Char('*'))); 0325 } 0326 QProcess *p = runPostInstallationCommand(scriptArgPath); 0327 connect(p, &QProcess::finished, this, [entry, installationFinished, this](int exitCode) { 0328 if (exitCode) { 0329 EntryInternal newEntry = entry; 0330 newEntry.setStatus(KNS3::Entry::Invalid); 0331 Q_EMIT signalEntryChanged(newEntry); 0332 } else { 0333 installationFinished(); 0334 } 0335 }); 0336 } else { 0337 installationFinished(); 0338 } 0339 } 0340 } 0341 0342 QString Installation::targetInstallationPath() const 0343 { 0344 // installdir is the target directory 0345 QString installdir; 0346 0347 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 79) 0348 const bool userScope = scope == ScopeUser; 0349 #else 0350 const bool userScope = true; 0351 #endif 0352 // installpath also contains the file name if it's a single file, otherwise equal to installdir 0353 int pathcounter = 0; 0354 // wallpaper is already managed in the case of !xdgTargetDirectory.isEmpty() 0355 if (!standardResourceDirectory.isEmpty() && standardResourceDirectory != QLatin1String("wallpaper")) { 0356 QStandardPaths::StandardLocation location = QStandardPaths::TempLocation; 0357 // crude translation KStandardDirs names -> QStandardPaths enum 0358 if (standardResourceDirectory == QLatin1String("tmp")) { 0359 location = QStandardPaths::TempLocation; 0360 } else if (standardResourceDirectory == QLatin1String("config")) { 0361 location = QStandardPaths::ConfigLocation; 0362 } 0363 0364 if (userScope) { 0365 installdir = QStandardPaths::writableLocation(location); 0366 } else { // system scope 0367 installdir = QStandardPaths::standardLocations(location).constLast(); 0368 } 0369 pathcounter++; 0370 } 0371 if (!targetDirectory.isEmpty() && targetDirectory != QLatin1String("/")) { 0372 if (userScope) { 0373 installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + targetDirectory + QLatin1Char('/'); 0374 } else { // system scope 0375 installdir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, targetDirectory, QStandardPaths::LocateDirectory) + QLatin1Char('/'); 0376 } 0377 pathcounter++; 0378 } 0379 if (!xdgTargetDirectory.isEmpty() && xdgTargetDirectory != QLatin1String("/")) { 0380 installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + xdgTargetDirectory + QLatin1Char('/'); 0381 pathcounter++; 0382 } 0383 if (!installPath.isEmpty()) { 0384 #if defined(Q_OS_WIN) 0385 WCHAR wPath[MAX_PATH + 1]; 0386 if (SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, wPath) == S_OK) { 0387 installdir = QString::fromUtf16((const ushort *)wPath) + QLatin1Char('/') + installPath + QLatin1Char('/'); 0388 } else { 0389 installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/'); 0390 } 0391 #else 0392 installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/'); 0393 #endif 0394 pathcounter++; 0395 } 0396 if (!absoluteInstallPath.isEmpty()) { 0397 installdir = absoluteInstallPath + QLatin1Char('/'); 0398 pathcounter++; 0399 } 0400 0401 if (pathcounter != 1) { 0402 qCCritical(KNEWSTUFFCORE) << "Wrong number of installation directories given."; 0403 return QString(); 0404 } 0405 0406 qCDebug(KNEWSTUFFCORE) << "installdir: " << installdir; 0407 0408 // create the dir if it doesn't exist (QStandardPaths doesn't create it, unlike KStandardDirs!) 0409 QDir().mkpath(installdir); 0410 0411 return installdir; 0412 } 0413 0414 QStringList Installation::installDownloadedFileAndUncompress(const KNSCore::EntryInternal &entry, const QString &payloadfile, const QString installdir) 0415 { 0416 // Collect all files that were installed 0417 QStringList installedFiles; 0418 bool isarchive = true; 0419 UncompressionOptions uncompressionOpt = uncompressionSetting(); 0420 0421 // respect the uncompress flag in the knsrc 0422 if (uncompressionOpt == UseKPackageUncompression) { 0423 qCDebug(KNEWSTUFFCORE) << "Using KPackage for installation"; 0424 KPackage::PackageStructure structure; 0425 KPackage::Package package(&structure); 0426 package.setPath(payloadfile); 0427 auto resetEntryStatus = [this, entry]() { 0428 KNSCore::EntryInternal changedEntry(entry); 0429 if (changedEntry.status() == KNS3::Entry::Installing || changedEntry.status() == KNS3::Entry::Installed) { 0430 changedEntry.setStatus(KNS3::Entry::Downloadable); 0431 } else if (changedEntry.status() == KNS3::Entry::Updating) { 0432 changedEntry.setStatus(KNS3::Entry::Updateable); 0433 } 0434 Q_EMIT signalEntryChanged(changedEntry); 0435 }; 0436 if (package.isValid() && package.metadata().isValid()) { 0437 qCDebug(KNEWSTUFFCORE) << "Package metadata is valid"; 0438 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 90) 0439 QString serviceType; 0440 serviceType = package.metadata().value(QStringLiteral("X-Plasma-ServiceType")); 0441 const auto serviceTypes = 0442 package.metadata().rawData().value(QLatin1String("KPlugin")).toObject().value(QLatin1String("ServiceTypes")).toVariant().toStringList(); 0443 if (serviceType.isEmpty() && !serviceTypes.isEmpty()) { 0444 serviceType = serviceTypes.first(); 0445 } 0446 if (serviceType.isEmpty()) { 0447 serviceType = property("kpackageType").toString(); 0448 } else if (serviceType != property("kpackageType").toString()) { 0449 qCWarning(KNEWSTUFFCORE) << "The package" << package.metadata().fileName() 0450 << "defines a different kpackage type than the one defined by the app." 0451 << "Please report this to the author of the addon."; 0452 } 0453 #else 0454 const QString serviceType = property("kpackageType").toString(); 0455 #endif 0456 0457 if (!serviceType.isEmpty()) { 0458 qCDebug(KNEWSTUFFCORE) << "Service type discovered as" << serviceType; 0459 KPackage::PackageStructure *structure = KPackage::PackageLoader::self()->loadPackageStructure(serviceType); 0460 if (structure) { 0461 KPackage::Package installer = KPackage::Package(structure); 0462 if (installer.hasValidStructure()) { 0463 QString packageRoot = 0464 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + installer.defaultPackageRoot(); 0465 qCDebug(KNEWSTUFFCORE) << "About to attempt to install" << package.metadata().pluginId() << "into" << packageRoot; 0466 const QString expectedDir{packageRoot + package.metadata().pluginId()}; 0467 KJob *installJob = KPackageJob::update(payloadfile, packageRoot, serviceType); 0468 // TODO KF6 Really, i would prefer to make more functions to handle this, but as this is 0469 // an exported class, i'd rather not pollute the public namespace with internal functions, 0470 // and we don't have a pimpl, so... we'll just have to deal with it for now 0471 connect(installJob, &KJob::result, this, [this, entry, payloadfile, expectedDir, resetEntryStatus](KJob *job) { 0472 if (job->error() == KJob::NoError) { 0473 if (QFile::exists(expectedDir)) { 0474 EntryInternal newentry = entry; 0475 newentry.setInstalledFiles(QStringList{expectedDir}); 0476 // update version and release date to the new ones 0477 if (newentry.status() == KNS3::Entry::Updating) { 0478 if (!newentry.updateVersion().isEmpty()) { 0479 newentry.setVersion(newentry.updateVersion()); 0480 } 0481 if (newentry.updateReleaseDate().isValid()) { 0482 newentry.setReleaseDate(newentry.updateReleaseDate()); 0483 } 0484 } 0485 newentry.setStatus(KNS3::Entry::Installed); 0486 // We can remove the downloaded file, because we don't save its location and don't need it to uninstall the entry 0487 QFile::remove(payloadfile); 0488 Q_EMIT signalEntryChanged(newentry); 0489 Q_EMIT signalInstallationFinished(); 0490 qCDebug(KNEWSTUFFCORE) << "Install job finished with no error and we now have files" << expectedDir; 0491 } else { 0492 Q_EMIT signalInstallationFailed( 0493 i18n("The installation of %1 failed to create the expected new directory %2").arg(payloadfile, expectedDir)); 0494 resetEntryStatus(); 0495 qCDebug(KNEWSTUFFCORE) 0496 << "Install job finished with no error, but we do not have the expected new directory" << expectedDir; 0497 } 0498 } else { 0499 if (job->error() == KPackage::Package::JobError::NewerVersionAlreadyInstalledError) { 0500 EntryInternal newentry = entry; 0501 newentry.setStatus(KNS3::Entry::Installed); 0502 newentry.setInstalledFiles(QStringList{expectedDir}); 0503 // update version and release date to the new ones 0504 if (!newentry.updateVersion().isEmpty()) { 0505 newentry.setVersion(newentry.updateVersion()); 0506 } 0507 if (newentry.updateReleaseDate().isValid()) { 0508 newentry.setReleaseDate(newentry.updateReleaseDate()); 0509 } 0510 Q_EMIT signalEntryChanged(newentry); 0511 Q_EMIT signalInstallationFinished(); 0512 qCDebug(KNEWSTUFFCORE) << "Install job finished telling us this item was already installed with this version, so... let's " 0513 "just make a small fib and say we totally installed that, honest, and we now have files" 0514 << expectedDir; 0515 } else { 0516 Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2", payloadfile, job->errorText())); 0517 resetEntryStatus(); 0518 qCDebug(KNEWSTUFFCORE) << "Install job finished with error state" << job->error() << "and description" << job->error(); 0519 } 0520 } 0521 }); 0522 installJob->start(); 0523 } else { 0524 Q_EMIT signalInstallationFailed( 0525 i18n("The installation of %1 failed, as the service type %2 was not accepted by the system (did you forget to install the KPackage " 0526 "support plugin for this type of package?)", 0527 payloadfile, 0528 serviceType)); 0529 resetEntryStatus(); 0530 qCWarning(KNEWSTUFFCORE) << "Package serviceType" << serviceType << "not found"; 0531 } 0532 } else { 0533 // no package structure 0534 Q_EMIT signalInstallationFailed( 0535 i18n("The installation of %1 failed, as the downloaded package does not contain a correct KPackage structure.", payloadfile)); 0536 resetEntryStatus(); 0537 qCWarning(KNEWSTUFFCORE) << "Could not load the package structure for KPackage service type" << serviceType; 0538 } 0539 } else { 0540 // no service type 0541 Q_EMIT signalInstallationFailed(i18n("The installation of %1 failed, as the downloaded package does not list a service type.", payloadfile)); 0542 resetEntryStatus(); 0543 qCWarning(KNEWSTUFFCORE) << "No service type listed in" << payloadfile; 0544 } 0545 } else { 0546 // package or package metadata is invalid 0547 Q_EMIT signalInstallationFailed( 0548 i18n("The installation of %1 failed, as the downloaded package does not contain any useful meta information, which means it is not a valid " 0549 "KPackage.", 0550 payloadfile)); 0551 resetEntryStatus(); 0552 qCWarning(KNEWSTUFFCORE) << "No valid meta information (which suggests no valid KPackage) found in" << payloadfile; 0553 } 0554 } else { 0555 if (uncompressionOpt == AlwaysUncompress || uncompressionOpt == UncompressIntoSubdirIfArchive || uncompressionOpt == UncompressIfArchive 0556 || uncompressionOpt == UncompressIntoSubdir) { 0557 // this is weird but a decompression is not a single name, so take the path instead 0558 QMimeDatabase db; 0559 QMimeType mimeType = db.mimeTypeForFile(payloadfile); 0560 qCDebug(KNEWSTUFFCORE) << "Postinstallation: uncompress the file"; 0561 0562 // FIXME: check for overwriting, malicious archive entries (../foo) etc. 0563 // FIXME: KArchive should provide "safe mode" for this! 0564 QScopedPointer<KArchive> archive; 0565 0566 if (mimeType.inherits(QStringLiteral("application/zip"))) { 0567 archive.reset(new KZip(payloadfile)); 0568 // clang-format off 0569 } else if (mimeType.inherits(QStringLiteral("application/tar")) 0570 || mimeType.inherits(QStringLiteral("application/x-tar")) // BUG 450662 0571 || mimeType.inherits(QStringLiteral("application/x-gzip")) 0572 || mimeType.inherits(QStringLiteral("application/x-bzip")) 0573 || mimeType.inherits(QStringLiteral("application/x-lzma")) 0574 || mimeType.inherits(QStringLiteral("application/x-xz")) 0575 || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) 0576 || mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) { 0577 // clang-format on 0578 archive.reset(new KTar(payloadfile)); 0579 } else { 0580 qCCritical(KNEWSTUFFCORE) << "Could not determine type of archive file '" << payloadfile << "'"; 0581 if (uncompressionOpt == AlwaysUncompress) { 0582 Q_EMIT signalInstallationError(i18n("Could not determine the type of archive of the downloaded file %1", payloadfile)); 0583 return QStringList(); 0584 } 0585 isarchive = false; 0586 } 0587 0588 if (isarchive) { 0589 bool success = archive->open(QIODevice::ReadOnly); 0590 if (!success) { 0591 qCCritical(KNEWSTUFFCORE) << "Cannot open archive file '" << payloadfile << "'"; 0592 if (uncompressionOpt == AlwaysUncompress) { 0593 Q_EMIT signalInstallationError( 0594 i18n("Failed to open the archive file %1. The reported error was: %2", payloadfile, archive->errorString())); 0595 return QStringList(); 0596 } 0597 // otherwise, just copy the file 0598 isarchive = false; 0599 } 0600 0601 if (isarchive) { 0602 const KArchiveDirectory *dir = archive->directory(); 0603 // if there is more than an item in the file, and we are requested to do so 0604 // put contents in a subdirectory with the same name as the file 0605 QString installpath; 0606 const bool isSubdir = 0607 (uncompressionOpt == UncompressIntoSubdir || uncompressionOpt == UncompressIntoSubdirIfArchive) && dir->entries().count() > 1; 0608 if (isSubdir) { 0609 installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName(); 0610 } else { 0611 installpath = installdir; 0612 } 0613 0614 if (dir->copyTo(installpath)) { 0615 // If we extracted the subdir we want to save it using the /* notation like we would when using the "archive" option 0616 // Also if we use an (un)install command we only call it once with the folder as argument and not for each file 0617 if (isSubdir) { 0618 installedFiles << QDir(installpath).absolutePath() + QLatin1String("/*"); 0619 } else { 0620 installedFiles << archiveEntries(installpath, dir); 0621 } 0622 } else { 0623 qCWarning(KNEWSTUFFCORE) << "could not install" << entry.name() << "to" << installpath; 0624 } 0625 0626 archive->close(); 0627 QFile::remove(payloadfile); 0628 } 0629 } 0630 } 0631 0632 qCDebug(KNEWSTUFFCORE) << "isarchive:" << isarchive; 0633 0634 // some wallpapers are compressed, some aren't 0635 if ((!isarchive && standardResourceDirectory == QLatin1String("wallpaper")) 0636 || (uncompressionOpt == NeverUncompress || (uncompressionOpt == UncompressIfArchive && !isarchive) 0637 || (uncompressionOpt == UncompressIntoSubdirIfArchive && !isarchive))) { 0638 // no decompress but move to target 0639 0640 /// @todo when using KIO::get the http header can be accessed and it contains a real file name. 0641 // FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names 0642 QUrl source = QUrl(entry.payload()); 0643 qCDebug(KNEWSTUFFCORE) << "installing non-archive from" << source; 0644 #if KNEWSTUFF_BUILD_DEPRECATED_SINCE(5, 79) 0645 QString installfile; 0646 QString ext = source.fileName().section(QLatin1Char('.'), -1); 0647 if (customName) { 0648 // Otherwise name can be interpreted as path 0649 installfile = entry.name().remove(QLatin1Char('/')); 0650 if (!entry.version().isEmpty()) { 0651 installfile += QLatin1Char('-') + entry.version(); 0652 } 0653 if (!ext.isEmpty()) { 0654 installfile += QLatin1Char('.') + ext; 0655 } 0656 } else { 0657 installfile = source.fileName(); 0658 } 0659 const QString installpath = QDir(installdir).filePath(installfile); 0660 #else 0661 const QString installpath = QDir(installdir).filePath(source.fileName()); 0662 #endif 0663 0664 qCDebug(KNEWSTUFFCORE) << "Install to file" << installpath; 0665 // FIXME: copy goes here (including overwrite checking) 0666 // FIXME: what must be done now is to update the cache *again* 0667 // in order to set the new payload filename (on root tag only) 0668 // - this might or might not need to take uncompression into account 0669 // FIXME: for updates, we might need to force an overwrite (that is, deleting before) 0670 QFile file(payloadfile); 0671 bool success = true; 0672 const bool update = ((entry.status() == KNS3::Entry::Updateable) || (entry.status() == KNS3::Entry::Updating)); 0673 0674 if (QFile::exists(installpath) && QDir::tempPath() != installdir) { 0675 if (!update) { 0676 Question question(Question::YesNoQuestion); 0677 question.setEntry(entry); 0678 question.setQuestion(i18n("This file already exists on disk (possibly due to an earlier failed download attempt). Continuing means " 0679 "overwriting it. Do you wish to overwrite the existing file?") 0680 + QStringLiteral("\n'") + installpath + QLatin1Char('\'')); 0681 question.setTitle(i18n("Overwrite File")); 0682 if (question.ask() != Question::YesResponse) { 0683 return QStringList(); 0684 } 0685 } 0686 success = QFile::remove(installpath); 0687 } 0688 if (success) { 0689 // remove in case it's already present and in a temporary directory, so we get to actually use the path again 0690 if (installpath.startsWith(QDir::tempPath())) { 0691 QFile::remove(installpath); 0692 } 0693 success = file.rename(installpath); 0694 qCDebug(KNEWSTUFFCORE) << "move:" << file.fileName() << "to" << installpath; 0695 if (!success) { 0696 qCWarning(KNEWSTUFFCORE) << file.errorString(); 0697 } 0698 } 0699 if (!success) { 0700 Q_EMIT signalInstallationError(i18n("Unable to move the file %1 to the intended destination %2", payloadfile, installpath)); 0701 qCCritical(KNEWSTUFFCORE) << "Cannot move file '" << payloadfile << "' to destination '" << installpath << "'"; 0702 return QStringList(); 0703 } 0704 installedFiles << installpath; 0705 } 0706 } 0707 0708 return installedFiles; 0709 } 0710 0711 QProcess *Installation::runPostInstallationCommand(const QString &installPath) 0712 { 0713 QString command(postInstallationCommand); 0714 QString fileArg(KShell::quoteArg(installPath)); 0715 command.replace(QLatin1String("%f"), fileArg); 0716 0717 qCDebug(KNEWSTUFFCORE) << "Run command:" << command; 0718 0719 QProcess *ret = new QProcess(this); 0720 auto onProcessFinished = [this, command, ret](int exitcode, QProcess::ExitStatus status) { 0721 const QString output{QString::fromLocal8Bit(ret->readAllStandardError())}; 0722 if (status == QProcess::CrashExit) { 0723 QString errorMessage = i18n("The installation failed while attempting to run the command:\n%1\n\nThe returned output was:\n%2", command, output); 0724 Q_EMIT signalInstallationError(errorMessage); 0725 qCCritical(KNEWSTUFFCORE) << "Process crashed with command: " << command; 0726 } else if (exitcode) { 0727 // 130 means Ctrl+C as an exit code this is interpreted by KNewStuff as cancel operation 0728 // and no error will be displayed to the user, BUG: 436355 0729 if (exitcode == 130) { 0730 qCCritical(KNEWSTUFFCORE) << "Command '" << command << "' failed was aborted by the user"; 0731 } else { 0732 Q_EMIT signalInstallationError( 0733 i18n("The installation failed with code %1 while attempting to run the command:\n%2\n\nThe returned output was:\n%3", 0734 exitcode, 0735 command, 0736 output)); 0737 qCCritical(KNEWSTUFFCORE) << "Command '" << command << "' failed with code" << exitcode; 0738 } 0739 } 0740 sender()->deleteLater(); 0741 }; 0742 connect(ret, &QProcess::finished, this, onProcessFinished); 0743 0744 QStringList args = KShell::splitArgs(command); 0745 ret->setProgram(args.takeFirst()); 0746 ret->setArguments(args); 0747 ret->start(); 0748 return ret; 0749 } 0750 0751 void Installation::uninstall(EntryInternal entry) 0752 { 0753 // TODO Put this in pimpl or job 0754 const auto deleteFilesAndMarkAsUninstalled = [entry, this]() { 0755 KNS3::Entry::Status newStatus{KNS3::Entry::Deleted}; 0756 const auto lst = entry.installedFiles(); 0757 for (const QString &file : lst) { 0758 // This is used to delete the download location if there are no more entries 0759 QFileInfo info(file); 0760 if (info.isDir()) { 0761 QDir().rmdir(file); 0762 } else if (file.endsWith(QLatin1String("/*"))) { 0763 QDir dir(file.left(file.size() - 2)); 0764 bool worked = dir.removeRecursively(); 0765 if (!worked) { 0766 qCWarning(KNEWSTUFFCORE) << "Couldn't remove" << dir.path(); 0767 continue; 0768 } 0769 } else { 0770 if (info.exists() || info.isSymLink()) { 0771 bool worked = QFile::remove(file); 0772 if (!worked) { 0773 qWarning() << "unable to delete file " << file; 0774 Q_EMIT signalInstallationFailed( 0775 i18n("The removal of %1 failed, as the installed file %2 could not be automatically removed. You can attempt to manually delete " 0776 "this file, if you believe this is an error.", 0777 entry.name(), 0778 file)); 0779 // Assume that the uninstallation has failed, and reset the entry to an installed state 0780 newStatus = KNS3::Entry::Installed; 0781 break; 0782 } 0783 } else { 0784 qWarning() << "unable to delete file " << file << ". file does not exist."; 0785 } 0786 } 0787 } 0788 EntryInternal newEntry = entry; 0789 if (newStatus == KNS3::Entry::Deleted) { 0790 newEntry.setUnInstalledFiles(entry.installedFiles()); 0791 newEntry.setInstalledFiles(QStringList()); 0792 } 0793 newEntry.setStatus(newStatus); 0794 Q_EMIT signalEntryChanged(newEntry); 0795 }; 0796 0797 if (uncompressionSetting() == UseKPackageUncompression) { 0798 const auto lst = entry.installedFiles(); 0799 if (lst.length() == 1) { 0800 const QString installedFile{lst.first()}; 0801 if (QFileInfo(installedFile).isDir()) { 0802 KPackage::PackageStructure structure; 0803 KPackage::Package package(&structure); 0804 package.setPath(installedFile); 0805 if (package.isValid() && package.metadata().isValid()) { 0806 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 90) 0807 QString serviceType = package.metadata().value(QStringLiteral("X-Plasma-ServiceType")); 0808 const auto serviceTypes = 0809 package.metadata().rawData().value(QLatin1String("KPlugin")).toObject().value(QLatin1String("ServiceTypes")).toVariant().toStringList(); 0810 if (serviceType.isEmpty() && !serviceTypes.isEmpty()) { 0811 serviceType = serviceTypes.first(); 0812 } 0813 if (serviceType.isEmpty()) { 0814 serviceType = property("kpackageType").toString(); 0815 } else if (serviceType != property("kpackageType").toString()) { 0816 qCWarning(KNEWSTUFFCORE) << "The package" << package.metadata().fileName() 0817 << "defines a different kpackage type than the one defined by the app." 0818 << "Please report this to the author of the addon."; 0819 } 0820 #else 0821 const QString serviceType = property("kpackageType").toString(); 0822 #endif 0823 if (!serviceType.isEmpty()) { 0824 KPackage::PackageStructure *structure = KPackage::PackageLoader::self()->loadPackageStructure(serviceType); 0825 if (structure) { 0826 KPackage::Package installer = KPackage::Package(structure); 0827 if (!installer.hasValidStructure()) { 0828 qWarning() << "Package serviceType" << serviceType << "not found"; 0829 } 0830 QString packageRoot = 0831 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + installer.defaultPackageRoot(); 0832 KJob *removalJob = KPackageJob::uninstall(package.metadata().pluginId(), packageRoot, serviceType); 0833 connect(removalJob, &KJob::result, this, [this, installedFile, installer, entry](KJob *job) { 0834 EntryInternal newEntry = entry; 0835 if (job->error() == KJob::NoError) { 0836 newEntry.setStatus(KNS3::Entry::Deleted); 0837 newEntry.setUnInstalledFiles(newEntry.installedFiles()); 0838 newEntry.setInstalledFiles(QStringList()); 0839 Q_EMIT signalEntryChanged(newEntry); 0840 } else { 0841 Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2", installedFile, job->errorText())); 0842 } 0843 }); 0844 removalJob->start(); 0845 } else { 0846 // no package structure 0847 Q_EMIT signalInstallationFailed( 0848 i18n("The removal of %1 failed, as the installed package does not contain a correct KPackage structure.", installedFile)); 0849 } 0850 } else { 0851 // no service type 0852 Q_EMIT signalInstallationFailed( 0853 i18n("The removal of %1 failed, as the installed package is not a supported type (did you forget to install the KPackage support " 0854 "plugin for this type of package?)", 0855 installedFile)); 0856 } 0857 } else { 0858 // package or package metadata is invalid 0859 Q_EMIT signalInstallationFailed( 0860 i18n("The removal of %1 failed, as the installed package does not contain any useful meta information, which means it is not a valid " 0861 "KPackage.", 0862 entry.name())); 0863 } 0864 } else { 0865 QMimeDatabase db; 0866 QMimeType mimeType = db.mimeTypeForFile(installedFile); 0867 if (mimeType.inherits(QStringLiteral("application/zip")) || mimeType.inherits(QStringLiteral("application/x-compressed-tar")) 0868 || mimeType.inherits(QStringLiteral("application/x-gzip")) || mimeType.inherits(QStringLiteral("application/x-tar")) 0869 || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || mimeType.inherits(QStringLiteral("application/x-xz")) 0870 || mimeType.inherits(QStringLiteral("application/x-lzma"))) { 0871 // Then it's one of the downloaded files installed with an old version of knewstuff prior to 0872 // the native kpackage support being added, and we need to do some inspection-and-removal work... 0873 KPackage::PackageStructure structure; 0874 KPackage::Package package(&structure); 0875 const QString serviceType{property("kpackageType").toString()}; 0876 package.setPath(installedFile); 0877 if (package.isValid() && package.metadata().isValid()) { 0878 // try and load the kpackage and sniff the expected location of its installation, and ask KPackage to remove that thing, if it's there 0879 KPackage::PackageStructure *structure = KPackage::PackageLoader::self()->loadPackageStructure(serviceType); 0880 if (structure) { 0881 KPackage::Package installer = KPackage::Package(structure); 0882 if (installer.hasValidStructure()) { 0883 QString packageRoot = 0884 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + installer.defaultPackageRoot(); 0885 qCDebug(KNEWSTUFFCORE) << "About to attempt to uninstall" << package.metadata().pluginId() << "from" << packageRoot; 0886 // Frankly, we don't care whether or not this next step succeeds, and it can just fizzle if it wants 0887 // to. This is a cleanup step, and if it fails, it's just not really important. 0888 KPackageJob::uninstall(package.metadata().pluginId(), packageRoot, serviceType); 0889 } 0890 } 0891 } 0892 // Also get rid of the downloaded file, and tell everything they've gone 0893 if (QFile::remove(installedFile)) { 0894 entry.setStatus(KNS3::Entry::Deleted); 0895 entry.setUnInstalledFiles(entry.installedFiles()); 0896 entry.setInstalledFiles(QStringList()); 0897 Q_EMIT signalEntryChanged(entry); 0898 } else { 0899 Q_EMIT signalInstallationFailed( 0900 i18n("The removal of %1 failed, as the downloaded file %2 could not be automatically removed.", entry.name(), installedFile)); 0901 } 0902 } else { 0903 // Not sure what's installed, but it's not a KPackage, not a lot we can do with this... 0904 Q_EMIT signalInstallationFailed( 0905 i18n("The removal of %1 failed, due to the installed file not being a KPackage. The file in question was %2, and you can attempt to " 0906 "delete it yourself, if you are certain that it is not needed.", 0907 entry.name(), 0908 installedFile)); 0909 } 0910 } 0911 } else { 0912 Q_EMIT signalInstallationFailed( 0913 i18n("The removal of %1 failed, as there seems to somehow be more than one thing installed, which is not supposed to be possible for KPackage " 0914 "based entries.", 0915 entry.name())); 0916 } 0917 deleteFilesAndMarkAsUninstalled(); 0918 } else { 0919 const auto lst = entry.installedFiles(); 0920 // If there is an uninstall script, make sure it runs without errors 0921 if (!uninstallCommand.isEmpty()) { 0922 bool validFileExisted = false; 0923 for (const QString &file : lst) { 0924 QString filePath = file; 0925 bool validFile = QFileInfo::exists(filePath); 0926 // If we have uncompressed a subdir we write <path>/* in the config, but when calling a script 0927 // we want to convert this to a normal path 0928 if (!validFile && file.endsWith(QLatin1Char('*'))) { 0929 filePath = filePath.left(filePath.lastIndexOf(QLatin1Char('*'))); 0930 validFile = QFileInfo::exists(filePath); 0931 } 0932 if (validFile) { 0933 validFileExisted = true; 0934 QString fileArg(KShell::quoteArg(filePath)); 0935 QString command(uninstallCommand); 0936 command.replace(QLatin1String("%f"), fileArg); 0937 0938 QStringList args = KShell::splitArgs(command); 0939 const QString program = args.takeFirst(); 0940 QProcess *process = new QProcess(this); 0941 process->start(program, args); 0942 auto onProcessFinished = [this, command, process, entry, deleteFilesAndMarkAsUninstalled](int, QProcess::ExitStatus status) { 0943 if (status == QProcess::CrashExit) { 0944 const QString processOutput = QString::fromLocal8Bit(process->readAllStandardError()); 0945 const QString err = i18n( 0946 "The uninstallation process failed to successfully run the command %1\n" 0947 "The output of was: \n%2\n" 0948 "If you think this is incorrect, you can continue or cancel the uninstallation process", 0949 KShell::quoteArg(command), 0950 processOutput); 0951 Q_EMIT signalInstallationError(err); 0952 // Ask the user if he wants to continue, even though the script failed 0953 Question question(Question::ContinueCancelQuestion); 0954 question.setEntry(entry); 0955 question.setQuestion(err); 0956 Question::Response response = question.ask(); 0957 if (response == Question::CancelResponse) { 0958 // Use can delete files manually 0959 EntryInternal newEntry = entry; 0960 newEntry.setStatus(KNS3::Entry::Installed); 0961 Q_EMIT signalEntryChanged(newEntry); 0962 return; 0963 } 0964 } else { 0965 qCDebug(KNEWSTUFFCORE) << "Command executed successfully:" << command; 0966 } 0967 deleteFilesAndMarkAsUninstalled(); 0968 }; 0969 connect(process, &QProcess::finished, this, onProcessFinished); 0970 } 0971 } 0972 // If the entry got deleted, but the RemoveDeadEntries option was not selected this case can happen 0973 if (!validFileExisted) { 0974 deleteFilesAndMarkAsUninstalled(); 0975 } 0976 } else { 0977 deleteFilesAndMarkAsUninstalled(); 0978 } 0979 } 0980 } 0981 0982 Installation::UncompressionOptions Installation::uncompressionSetting() const 0983 { 0984 return property("uncompressSetting").value<UncompressionOptions>(); 0985 } 0986 0987 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 31) 0988 void Installation::slotInstallationVerification(int result) 0989 { 0990 Q_UNUSED(result) 0991 // Deprecated, was wired up to defunct Security class. 0992 } 0993 #endif 0994 0995 QStringList Installation::archiveEntries(const QString &path, const KArchiveDirectory *dir) 0996 { 0997 QStringList files; 0998 const auto lst = dir->entries(); 0999 for (const QString &entry : lst) { 1000 const auto currentEntry = dir->entry(entry); 1001 1002 const QString childPath = QDir(path).filePath(entry); 1003 if (currentEntry->isFile()) { 1004 files << childPath; 1005 } else if (currentEntry->isDirectory()) { 1006 files << childPath + QStringLiteral("/*"); 1007 } 1008 } 1009 return files; 1010 } 1011 1012 #include "moc_installation.cpp"