File indexing completed on 2024-09-15 03:39:35
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"