File indexing completed on 2024-09-29 12:09:46

0001 /*
0002     SPDX-FileCopyrightText: 2007-2009 Aaron Seigo <aseigo@kde.org>
0003     SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "private/packagejobthread_p.h"
0009 #include "private/utils.h"
0010 
0011 #include "config-package.h"
0012 #include "package.h"
0013 
0014 #include <KArchive>
0015 #include <KLocalizedString>
0016 #include <KTar>
0017 #include <kzip.h>
0018 
0019 
0020 #include "kpackage_debug.h"
0021 #include <QDir>
0022 #include <QFile>
0023 #include <QIODevice>
0024 #include <QJsonDocument>
0025 #include <QMimeDatabase>
0026 #include <QMimeType>
0027 #include <QProcess>
0028 #include <QRegularExpression>
0029 #include <QStandardPaths>
0030 #include <QUrl>
0031 #include <qtemporarydir.h>
0032 
0033 namespace KPackage
0034 {
0035 bool copyFolder(QString sourcePath, QString targetPath)
0036 {
0037     QDir source(sourcePath);
0038     if (!source.exists()) {
0039         return false;
0040     }
0041 
0042     QDir target(targetPath);
0043     if (!target.exists()) {
0044         QString targetName = target.dirName();
0045         target.cdUp();
0046         target.mkdir(targetName);
0047         target = QDir(targetPath);
0048     }
0049 
0050     const auto lstEntries = source.entryList(QDir::Files);
0051     for (const QString &fileName : lstEntries) {
0052         QString sourceFilePath = sourcePath + QDir::separator() + fileName;
0053         QString targetFilePath = targetPath + QDir::separator() + fileName;
0054 
0055         if (!QFile::copy(sourceFilePath, targetFilePath)) {
0056             return false;
0057         }
0058     }
0059     const auto lstEntries2 = source.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
0060     for (const QString &subFolderName : lstEntries2) {
0061         QString sourceSubFolderPath = sourcePath + QDir::separator() + subFolderName;
0062         QString targetSubFolderPath = targetPath + QDir::separator() + subFolderName;
0063 
0064         if (!copyFolder(sourceSubFolderPath, targetSubFolderPath)) {
0065             return false;
0066         }
0067     }
0068 
0069     return true;
0070 }
0071 
0072 bool removeFolder(QString folderPath)
0073 {
0074     QDir folder(folderPath);
0075     return folder.removeRecursively();
0076 }
0077 
0078 Q_GLOBAL_STATIC_WITH_ARGS(QStringList, metaDataFiles, (QStringList(QLatin1String("metadata.desktop")) << QLatin1String("metadata.json")))
0079 
0080 
0081 class PackageJobThreadPrivate
0082 {
0083 public:
0084     QString installPath;
0085     QString errorMessage;
0086     int errorCode;
0087 };
0088 
0089 PackageJobThread::PackageJobThread(QObject *parent)
0090     : QThread(parent)
0091 {
0092     d = new PackageJobThreadPrivate;
0093     d->errorCode = KJob::NoError;
0094 }
0095 
0096 PackageJobThread::~PackageJobThread()
0097 {
0098     delete d;
0099 }
0100 
0101 bool PackageJobThread::install(const QString &src, const QString &dest)
0102 {
0103     bool ok = installPackage(src, dest, Install);
0104     Q_EMIT installPathChanged(d->installPath);
0105     Q_EMIT jobThreadFinished(ok, d->errorMessage);
0106     return ok;
0107 }
0108 
0109 static QString resolveHandler(const QString &scheme)
0110 {
0111     QString envOverride = qEnvironmentVariable("KPACKAGE_DEP_RESOLVERS_PATH");
0112     QStringList searchDirs;
0113     if (!envOverride.isEmpty()) {
0114         searchDirs.push_back(envOverride);
0115     }
0116     searchDirs.append(QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kpackagehandlers"));
0117     // We have to use QStandardPaths::findExecutable here to handle the .exe suffix on Windows.
0118     return QStandardPaths::findExecutable(scheme + QLatin1String("handler"), searchDirs);
0119 }
0120 
0121 bool PackageJobThread::installDependency(const QUrl &destUrl)
0122 {
0123     auto handler = resolveHandler(destUrl.scheme());
0124     if (handler.isEmpty()) {
0125         return false;
0126     }
0127 
0128     QProcess process;
0129     process.setProgram(handler);
0130     process.setArguments({destUrl.toString()});
0131     process.setProcessChannelMode(QProcess::ForwardedChannels);
0132     process.start();
0133     process.waitForFinished();
0134 
0135     return process.exitCode() == 0;
0136 }
0137 
0138 bool PackageJobThread::installPackage(const QString &src, const QString &dest, OperationType operation)
0139 {
0140     QDir root(dest);
0141     if (!root.exists()) {
0142         QDir().mkpath(dest);
0143         if (!root.exists()) {
0144             d->errorMessage = i18n("Could not create package root directory: %1", dest);
0145             d->errorCode = Package::JobError::RootCreationError;
0146             // qCWarning(KPACKAGE_LOG) << "Could not create package root directory: " << dest;
0147             return false;
0148         }
0149     }
0150 
0151     QFileInfo fileInfo(src);
0152     if (!fileInfo.exists()) {
0153         d->errorMessage = i18n("No such file: %1", src);
0154         d->errorCode = Package::JobError::PackageFileNotFoundError;
0155         return false;
0156     }
0157 
0158     QString path;
0159     QTemporaryDir tempdir;
0160     bool archivedPackage = false;
0161 
0162     if (fileInfo.isDir()) {
0163         // we have a directory, so let's just install what is in there
0164         path = src;
0165         // make sure we end in a slash!
0166         if (!path.endsWith(QLatin1Char('/'))) {
0167             path.append(QLatin1Char('/'));
0168         }
0169     } else {
0170         KArchive *archive = nullptr;
0171         QMimeDatabase db;
0172         QMimeType mimetype = db.mimeTypeForFile(src);
0173         if (mimetype.inherits(QStringLiteral("application/zip"))) {
0174             archive = new KZip(src);
0175         } else if (mimetype.inherits(QStringLiteral("application/x-compressed-tar")) || //
0176                    mimetype.inherits(QStringLiteral("application/x-tar")) || //
0177                    mimetype.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || //
0178                    mimetype.inherits(QStringLiteral("application/x-xz")) || //
0179                    mimetype.inherits(QStringLiteral("application/x-lzma"))) {
0180             archive = new KTar(src);
0181         } else {
0182             // qCWarning(KPACKAGE_LOG) << "Could not open package file, unsupported archive format:" << src << mimetype.name();
0183             d->errorMessage = i18n("Could not open package file, unsupported archive format: %1 %2", src, mimetype.name());
0184             d->errorCode = Package::JobError::UnsupportedArchiveFormatError;
0185             return false;
0186         }
0187 
0188         if (!archive->open(QIODevice::ReadOnly)) {
0189             // qCWarning(KPACKAGE_LOG) << "Could not open package file:" << src;
0190             delete archive;
0191             d->errorMessage = i18n("Could not open package file: %1", src);
0192             d->errorCode = Package::JobError::PackageOpenError;
0193             return false;
0194         }
0195 
0196         archivedPackage = true;
0197         path = tempdir.path() + QLatin1Char('/');
0198 
0199         d->installPath = path;
0200 
0201         const KArchiveDirectory *source = archive->directory();
0202         source->copyTo(path);
0203 
0204         QStringList entries = source->entries();
0205         if (entries.count() == 1) {
0206             const KArchiveEntry *entry = source->entry(entries[0]);
0207             if (entry->isDirectory()) {
0208                 path = path + entry->name() + QLatin1Char('/');
0209             }
0210         }
0211 
0212         delete archive;
0213     }
0214 
0215     QDir packageDir(path);
0216     QFileInfoList entries = packageDir.entryInfoList(*metaDataFiles);
0217     KPluginMetaData meta;
0218     if (!entries.isEmpty()) {
0219         const QString metadataFilePath = entries.first().filePath();
0220 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 92)
0221         if (metadataFilePath.endsWith(QLatin1String(".desktop"))) {
0222             meta = KPluginMetaData::fromDesktopFile(metadataFilePath, {QStringLiteral(":/kservicetypes5/kpackage-generic.desktop")});
0223         } else {
0224             meta = KPluginMetaData::fromJsonFile(metadataFilePath);
0225         }
0226 #else
0227         meta = KPluginMetaData::fromJsonFile(metadataFilePath);
0228 #endif
0229     } else {
0230         qCWarning(KPACKAGE_LOG) << "Couldn't open metadata file" << src << path;
0231         d->errorMessage = i18n("Could not open metadata file: %1", src);
0232         d->errorCode = Package::JobError::MetadataFileMissingError;
0233         return false;
0234     }
0235 
0236     if (!meta.isValid()) {
0237         qCDebug(KPACKAGE_LOG) << "No metadata file in package" << src << path;
0238         d->errorMessage = i18n("No metadata file in package: %1", src);
0239         d->errorCode = Package::JobError::MetadataFileMissingError;
0240         return false;
0241     }
0242 
0243     QString pluginName = meta.pluginId();
0244     qCDebug(KPACKAGE_LOG) << "pluginname: " << meta.pluginId();
0245     if (pluginName.isEmpty()) {
0246         // qCWarning(KPACKAGE_LOG) << "Package plugin name not specified";
0247         d->errorMessage = i18n("Package plugin name not specified: %1", src);
0248         d->errorCode = Package::JobError::PluginNameMissingError;
0249         return false;
0250     }
0251 
0252     // Ensure that package names are safe so package uninstall can't inject
0253     // bad characters into the paths used for removal.
0254     const QRegularExpression validatePluginName(QStringLiteral("^[\\w\\-\\.]+$")); // Only allow letters, numbers, underscore and period.
0255     if (!validatePluginName.match(pluginName).hasMatch()) {
0256         // qCDebug(KPACKAGE_LOG) << "Package plugin name " << pluginName << "contains invalid characters";
0257         d->errorMessage = i18n("Package plugin name %1 contains invalid characters", pluginName);
0258         d->errorCode = Package::JobError::PluginNameInvalidError;
0259         return false;
0260     }
0261 
0262     QString targetName = dest;
0263     if (targetName[targetName.size() - 1] != QLatin1Char('/')) {
0264         targetName.append(QLatin1Char('/'));
0265     }
0266     targetName.append(pluginName);
0267 
0268     if (QFile::exists(targetName)) {
0269         if (operation == Update) {
0270             KPluginMetaData oldMeta;
0271             if (QFileInfo::exists(targetName + QLatin1String("/metadata.json"))) {
0272                 oldMeta = KPluginMetaData::fromJsonFile(targetName + QLatin1String("/metadata.json"));
0273 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 92)
0274             } else if (QFileInfo::exists(targetName + QLatin1String("/metadata.desktop"))) {
0275                 oldMeta = KPluginMetaData::fromDesktopFile(targetName + QLatin1String("/metadata.desktop"));
0276 #endif
0277             }
0278 
0279             if (readKPackageTypes(oldMeta) != readKPackageTypes(meta)) {
0280                 d->errorMessage = i18n("The new package has a different type from the old version already installed.");
0281                 d->errorCode = Package::JobError::UpdatePackageTypeMismatchError;
0282             } else if (isVersionNewer(oldMeta.version(), meta.version())) {
0283                 const bool ok = uninstallPackage(targetName);
0284                 if (!ok) {
0285                     d->errorMessage = i18n("Impossible to remove the old installation of %1 located at %2. error: %3", pluginName, targetName, d->errorMessage);
0286                     d->errorCode = Package::JobError::OldVersionRemovalError;
0287                 }
0288             } else {
0289                 d->errorMessage = i18n("Not installing version %1 of %2. Version %3 already installed.", meta.version(), meta.pluginId(), oldMeta.version());
0290                 d->errorCode = Package::JobError::NewerVersionAlreadyInstalledError;
0291             }
0292         } else {
0293             d->errorMessage = i18n("%1 already exists", targetName);
0294             d->errorCode = Package::JobError::PackageAlreadyInstalledError;
0295         }
0296 
0297         if (d->errorCode != KJob::NoError) {
0298             d->installPath = targetName;
0299             return false;
0300         }
0301     }
0302 
0303     // install dependencies
0304     const QStringList optionalDependencies{QStringLiteral("sddmtheme.knsrc")};
0305     const QStringList dependencies = meta.value(QStringLiteral("X-KPackage-Dependencies"), QStringList());
0306     for (const QString &dep : dependencies) {
0307         QUrl depUrl(dep);
0308         const QString knsrcFilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("knsrcfiles/") + depUrl.host());
0309         if (knsrcFilePath.isEmpty() && optionalDependencies.contains(depUrl.host())) {
0310             qWarning() << "Skipping depdendency due to knsrc files being missing" << depUrl;
0311             continue;
0312         }
0313         if (!installDependency(depUrl)) {
0314             d->errorMessage = i18n("Could not install dependency: '%1'", dep);
0315             d->errorCode = Package::JobError::PackageCopyError;
0316             return false;
0317         }
0318     }
0319 
0320     if (archivedPackage) {
0321         // it's in a temp dir, so just move it over.
0322         const bool ok = copyFolder(path, targetName);
0323         removeFolder(path);
0324         if (!ok) {
0325             // qCWarning(KPACKAGE_LOG) << "Could not move package to destination:" << targetName;
0326             d->errorMessage = i18n("Could not move package to destination: %1", targetName);
0327             d->errorCode = Package::JobError::PackageMoveError;
0328             return false;
0329         }
0330     } else {
0331         // it's a directory containing the stuff, so copy the contents rather
0332         // than move them
0333         const bool ok = copyFolder(path, targetName);
0334         if (!ok) {
0335             // qCWarning(KPACKAGE_LOG) << "Could not copy package to destination:" << targetName;
0336             d->errorMessage = i18n("Could not copy package to destination: %1", targetName);
0337             d->errorCode = Package::JobError::PackageCopyError;
0338             return false;
0339         }
0340     }
0341 
0342     if (archivedPackage) {
0343         // no need to remove the temp dir (which has been successfully moved if it's an archive)
0344         tempdir.setAutoRemove(false);
0345     }
0346 
0347     d->installPath = targetName;
0348     return true;
0349 }
0350 
0351 bool PackageJobThread::update(const QString &src, const QString &dest)
0352 {
0353     bool ok = installPackage(src, dest, Update);
0354     Q_EMIT installPathChanged(d->installPath);
0355     Q_EMIT jobThreadFinished(ok, d->errorMessage);
0356     return ok;
0357 }
0358 
0359 bool PackageJobThread::uninstall(const QString &packagePath)
0360 {
0361     bool ok = uninstallPackage(packagePath);
0362     // qCDebug(KPACKAGE_LOG) << "emit installPathChanged " << d->installPath;
0363     Q_EMIT installPathChanged(QString());
0364     // qCDebug(KPACKAGE_LOG) << "Thread: installFinished" << ok;
0365     Q_EMIT jobThreadFinished(ok, d->errorMessage);
0366     return ok;
0367 }
0368 
0369 bool PackageJobThread::uninstallPackage(const QString &packagePath)
0370 {
0371     if (!QFile::exists(packagePath)) {
0372         d->errorMessage = packagePath.isEmpty() ? i18n("package path was deleted manually") : i18n("%1 does not exist", packagePath);
0373         d->errorCode = Package::JobError::PackageFileNotFoundError;
0374         return false;
0375     }
0376     QString pkg;
0377     QString root;
0378     {
0379         // TODO KF6 remove, pass in packageroot, type and pluginName separately?
0380         QStringList ps = packagePath.split(QLatin1Char('/'));
0381         int ix = ps.count() - 1;
0382         if (packagePath.endsWith(QLatin1Char('/'))) {
0383             ix = ps.count() - 2;
0384         }
0385         pkg = ps[ix];
0386         ps.pop_back();
0387         root = ps.join(QLatin1Char('/'));
0388     }
0389 
0390     bool ok = removeFolder(packagePath);
0391     if (!ok) {
0392         d->errorMessage = i18n("Could not delete package from: %1", packagePath);
0393         d->errorCode = Package::JobError::PackageUninstallError;
0394         return false;
0395     }
0396 
0397     return true;
0398 }
0399 
0400 Package::JobError PackageJobThread::errorCode() const
0401 {
0402     return static_cast<Package::JobError>(d->errorCode);
0403 }
0404 
0405 } // namespace KPackage
0406 
0407 #include "moc_packagejobthread_p.cpp"