File indexing completed on 2024-04-28 03:56:34

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