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"