KPackage

packagejobthread.cpp
1/*
2 SPDX-FileCopyrightText: 2007-2009 Aaron Seigo <aseigo@kde.org>
3 SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "private/packagejobthread_p.h"
9#include "private/utils.h"
10
11#include "config-package.h"
12#include "package.h"
13
14#include <KArchive>
15#include <KLocalizedString>
16#include <KTar>
17#include <kzip.h>
18
19#include "kpackage_debug.h"
20#include <QDir>
21#include <QFile>
22#include <QIODevice>
23#include <QJsonDocument>
24#include <QMimeDatabase>
25#include <QMimeType>
26#include <QProcess>
27#include <QRegularExpression>
28#include <QStandardPaths>
29#include <QUrl>
30#include <qtemporarydir.h>
31
32namespace KPackage
33{
34bool copyFolder(QString sourcePath, QString targetPath)
35{
36 QDir source(sourcePath);
37 if (!source.exists()) {
38 return false;
39 }
40
41 QDir target(targetPath);
42 if (!target.exists()) {
43 QString targetName = target.dirName();
44 target.cdUp();
45 target.mkdir(targetName);
46 target = QDir(targetPath);
47 }
48
49 const auto lstEntries = source.entryList(QDir::Files);
50 for (const QString &fileName : lstEntries) {
51 QString sourceFilePath = sourcePath + QDir::separator() + fileName;
52 QString targetFilePath = targetPath + QDir::separator() + fileName;
53
54 if (!QFile::copy(sourceFilePath, targetFilePath)) {
55 return false;
56 }
57 }
58 const auto lstEntries2 = source.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
59 for (const QString &subFolderName : lstEntries2) {
60 QString sourceSubFolderPath = sourcePath + QDir::separator() + subFolderName;
61 QString targetSubFolderPath = targetPath + QDir::separator() + subFolderName;
62
63 if (!copyFolder(sourceSubFolderPath, targetSubFolderPath)) {
64 return false;
65 }
66 }
67
68 return true;
69}
70
71bool removeFolder(QString folderPath)
72{
73 QDir folder(folderPath);
74 return folder.removeRecursively();
75}
76
77class PackageJobThreadPrivate
78{
79public:
80 QString installPath;
81 QString errorMessage;
82 std::function<void()> run;
83 int errorCode;
84};
85
86PackageJobThread::PackageJobThread(PackageJob::OperationType type, const QString &src, const QString &dest, const KPackage::Package &package)
87 : QObject()
88 , QRunnable()
89{
90 d = new PackageJobThreadPrivate;
91 d->errorCode = KJob::NoError;
92 if (type == PackageJob::Install) {
93 d->run = [this, src, dest, package]() {
94 install(src, dest, package);
95 };
96 } else if (type == PackageJob::Update) {
97 d->run = [this, src, dest, package]() {
98 update(src, dest, package);
99 };
100 } else if (type == PackageJob::Uninstall) {
101 const QString packagePath = package.path();
102 d->run = [this, packagePath]() {
103 uninstall(packagePath);
104 };
105
106 } else {
107 Q_UNREACHABLE();
108 }
109}
110
111PackageJobThread::~PackageJobThread()
112{
113 delete d;
114}
115
116void PackageJobThread::run()
117{
118 Q_ASSERT(d->run);
119 d->run();
120}
121bool PackageJobThread::install(const QString &src, const QString &dest, const Package &package)
122{
123 bool ok = installPackage(src, dest, package, PackageJob::Install);
124 Q_EMIT installPathChanged(d->installPath);
125 Q_EMIT jobThreadFinished(ok, errorCode(), d->errorMessage);
126 return ok;
127}
128
129static QString resolveHandler(const QString &scheme)
130{
131 QString envOverride = qEnvironmentVariable("KPACKAGE_DEP_RESOLVERS_PATH");
132 QStringList searchDirs;
133 if (!envOverride.isEmpty()) {
134 searchDirs.push_back(envOverride);
135 }
136 searchDirs.append(QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kpackagehandlers"));
137 // We have to use QStandardPaths::findExecutable here to handle the .exe suffix on Windows.
138 return QStandardPaths::findExecutable(scheme + QLatin1String("handler"), searchDirs);
139}
140
141bool PackageJobThread::installDependency(const QUrl &destUrl)
142{
143 auto handler = resolveHandler(destUrl.scheme());
144 if (handler.isEmpty()) {
145 return false;
146 }
147
148 QProcess process;
149 process.setProgram(handler);
150 process.setArguments({destUrl.toString()});
152 process.start();
153 process.waitForFinished();
154
155 return process.exitCode() == 0;
156}
157
158bool PackageJobThread::installPackage(const QString &src, const QString &dest, const Package &package, PackageJob::OperationType operation)
159{
160 QDir root(dest);
161 if (!root.exists()) {
162 QDir().mkpath(dest);
163 if (!root.exists()) {
164 d->errorMessage = i18n("Could not create package root directory: %1", dest);
165 d->errorCode = PackageJob::JobError::RootCreationError;
166 // qCWarning(KPACKAGE_LOG) << "Could not create package root directory: " << dest;
167 return false;
168 }
169 }
170
171 QFileInfo fileInfo(src);
172 if (!fileInfo.exists()) {
173 d->errorMessage = i18n("No such file: %1", src);
174 d->errorCode = PackageJob::JobError::PackageFileNotFoundError;
175 return false;
176 }
177
179 QTemporaryDir tempdir;
180 bool archivedPackage = false;
181
182 if (fileInfo.isDir()) {
183 // we have a directory, so let's just install what is in there
184 path = src;
185 // make sure we end in a slash!
186 if (!path.endsWith(QLatin1Char('/'))) {
187 path.append(QLatin1Char('/'));
188 }
189 } else {
190 KArchive *archive = nullptr;
191 QMimeDatabase db;
193 if (mimetype.inherits(QStringLiteral("application/zip"))) {
194 archive = new KZip(src);
195 } else if (mimetype.inherits(QStringLiteral("application/x-compressed-tar")) || //
196 mimetype.inherits(QStringLiteral("application/x-tar")) || //
197 mimetype.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || //
198 mimetype.inherits(QStringLiteral("application/x-xz")) || //
199 mimetype.inherits(QStringLiteral("application/x-lzma"))) {
200 archive = new KTar(src);
201 } else {
202 // qCWarning(KPACKAGE_LOG) << "Could not open package file, unsupported archive format:" << src << mimetype.name();
203 d->errorMessage = i18n("Could not open package file, unsupported archive format: %1 %2", src, mimetype.name());
204 d->errorCode = PackageJob::JobError::UnsupportedArchiveFormatError;
205 return false;
206 }
207
208 if (!archive->open(QIODevice::ReadOnly)) {
209 // qCWarning(KPACKAGE_LOG) << "Could not open package file:" << src;
210 delete archive;
211 d->errorMessage = i18n("Could not open package file: %1", src);
212 d->errorCode = PackageJob::JobError::PackageOpenError;
213 return false;
214 }
215
216 archivedPackage = true;
217 path = tempdir.path() + QLatin1Char('/');
218
219 d->installPath = path;
220
221 const KArchiveDirectory *source = archive->directory();
222 source->copyTo(path);
223
224 QStringList entries = source->entries();
225 if (entries.count() == 1) {
226 const KArchiveEntry *entry = source->entry(entries[0]);
227 if (entry->isDirectory()) {
228 path = path + entry->name() + QLatin1Char('/');
229 }
230 }
231
232 delete archive;
233 }
234
235 Package copyPackage = package;
236 copyPackage.setPath(path);
237 if (!copyPackage.isValid()) {
238 d->errorMessage = i18n("Package is not considered valid");
239 d->errorCode = PackageJob::JobError::InvalidPackageStructure;
240 return false;
241 }
242
243 KPluginMetaData meta = copyPackage.metadata(); // The packagestructure might have set the metadata, so use that
244 QString pluginName = meta.pluginId().isEmpty() ? QFileInfo(src).baseName() : meta.pluginId();
245 qCDebug(KPACKAGE_LOG) << "pluginname: " << meta.pluginId();
246 if (pluginName == QLatin1String("metadata")) {
247 // qCWarning(KPACKAGE_LOG) << "Package plugin id not specified";
248 d->errorMessage = i18n("Package plugin id not specified: %1", src);
249 d->errorCode = PackageJob::JobError::PluginIdInvalidError;
250 return false;
251 }
252
253 // Ensure that package names are safe so package uninstall can't inject
254 // bad characters into the paths used for removal.
255 const QRegularExpression validatePluginName(QStringLiteral("^[\\w\\-\\.]+$")); // Only allow letters, numbers, underscore and period.
256 if (!validatePluginName.match(pluginName).hasMatch()) {
257 // qCDebug(KPACKAGE_LOG) << "Package plugin id " << pluginName << "contains invalid characters";
258 d->errorMessage = i18n("Package plugin id %1 contains invalid characters", pluginName);
259 d->errorCode = PackageJob::JobError::PluginIdInvalidError;
260 return false;
261 }
262
263 QString targetName = dest;
264 if (targetName[targetName.size() - 1] != QLatin1Char('/')) {
265 targetName.append(QLatin1Char('/'));
266 }
267 targetName.append(pluginName);
268
269 if (QFile::exists(targetName)) {
270 if (operation == PackageJob::Update) {
271 KPluginMetaData oldMeta;
272 if (QString jsonPath = targetName + QLatin1String("/metadata.json"); QFileInfo::exists(jsonPath)) {
273 oldMeta = KPluginMetaData::fromJsonFile(jsonPath);
274 }
275
276 if (readKPackageType(oldMeta) != readKPackageType(meta)) {
277 d->errorMessage = i18n("The new package has a different type from the old version already installed.");
278 d->errorCode = PackageJob::JobError::UpdatePackageTypeMismatchError;
279 } else if (isVersionNewer(oldMeta.version(), meta.version())) {
280 const bool ok = uninstallPackage(targetName);
281 if (!ok) {
282 d->errorMessage = i18n("Impossible to remove the old installation of %1 located at %2. error: %3", pluginName, targetName, d->errorMessage);
283 d->errorCode = PackageJob::JobError::OldVersionRemovalError;
284 }
285 } else {
286 d->errorMessage = i18n("Not installing version %1 of %2. Version %3 already installed.", meta.version(), meta.pluginId(), oldMeta.version());
287 d->errorCode = PackageJob::JobError::NewerVersionAlreadyInstalledError;
288 }
289 } else {
290 d->errorMessage = i18n("%1 already exists", targetName);
291 d->errorCode = PackageJob::JobError::PackageAlreadyInstalledError;
292 }
293
294 if (d->errorCode != KJob::NoError) {
295 d->installPath = targetName;
296 return false;
297 }
298 }
299
300 // install dependencies
301 const QStringList optionalDependencies{QStringLiteral("sddmtheme.knsrc")};
302 const QStringList dependencies = meta.value(QStringLiteral("X-KPackage-Dependencies"), QStringList());
303 for (const QString &dep : dependencies) {
304 QUrl depUrl(dep);
305 const QString knsrcFilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("knsrcfiles/") + depUrl.host());
306 if (knsrcFilePath.isEmpty() && optionalDependencies.contains(depUrl.host())) {
307 qWarning() << "Skipping depdendency due to knsrc files being missing" << depUrl;
308 continue;
309 }
310 if (!installDependency(depUrl)) {
311 d->errorMessage = i18n("Could not install dependency: '%1'", dep);
312 d->errorCode = PackageJob::JobError::PackageCopyError;
313 return false;
314 }
315 }
316
317 if (archivedPackage) {
318 // it's in a temp dir, so just move it over.
319 const bool ok = copyFolder(path, targetName);
320 removeFolder(path);
321 if (!ok) {
322 // qCWarning(KPACKAGE_LOG) << "Could not move package to destination:" << targetName;
323 d->errorMessage = i18n("Could not move package to destination: %1", targetName);
324 d->errorCode = PackageJob::JobError::PackageMoveError;
325 return false;
326 }
327 } else {
328 // it's a directory containing the stuff, so copy the contents rather
329 // than move them
330 const bool ok = copyFolder(path, targetName);
331 if (!ok) {
332 // qCWarning(KPACKAGE_LOG) << "Could not copy package to destination:" << targetName;
333 d->errorMessage = i18n("Could not copy package to destination: %1", targetName);
334 d->errorCode = PackageJob::JobError::PackageCopyError;
335 return false;
336 }
337 }
338
339 if (archivedPackage) {
340 // no need to remove the temp dir (which has been successfully moved if it's an archive)
341 tempdir.setAutoRemove(false);
342 }
343
344 d->installPath = targetName;
345 return true;
346}
347
348bool PackageJobThread::update(const QString &src, const QString &dest, const Package &package)
349{
350 bool ok = installPackage(src, dest, package, PackageJob::Update);
351 Q_EMIT installPathChanged(d->installPath);
352 Q_EMIT jobThreadFinished(ok, errorCode(), d->errorMessage);
353 return ok;
354}
355
356bool PackageJobThread::uninstall(const QString &packagePath)
357{
358 bool ok = uninstallPackage(packagePath);
359 // Do not emit the install path changed, information about the removed package might be useful for consumers
360 // qCDebug(KPACKAGE_LOG) << "Thread: installFinished" << ok;
361 Q_EMIT jobThreadFinished(ok, errorCode(), d->errorMessage);
362 return ok;
363}
364
365bool PackageJobThread::uninstallPackage(const QString &packagePath)
366{
367 if (!QFile::exists(packagePath)) {
368 d->errorMessage = packagePath.isEmpty() ? i18n("package path was deleted manually") : i18n("%1 does not exist", packagePath);
369 d->errorCode = PackageJob::JobError::PackageFileNotFoundError;
370 return false;
371 }
372 QString pkg;
373 QString root;
374 {
375 // TODO KF6 remove, pass in packageroot, type and pluginName separately?
376 QStringList ps = packagePath.split(QLatin1Char('/'));
377 int ix = ps.count() - 1;
378 if (packagePath.endsWith(QLatin1Char('/'))) {
379 ix = ps.count() - 2;
380 }
381 pkg = ps[ix];
382 ps.pop_back();
383 root = ps.join(QLatin1Char('/'));
384 }
385
386 bool ok = removeFolder(packagePath);
387 if (!ok) {
388 d->errorMessage = i18n("Could not delete package from: %1", packagePath);
389 d->errorCode = PackageJob::JobError::PackageUninstallError;
390 return false;
391 }
392
393 return true;
394}
395
396PackageJob::JobError PackageJobThread::errorCode() const
397{
398 return static_cast<PackageJob::JobError>(d->errorCode);
399}
400
401} // namespace KPackage
402
403#include "moc_packagejobthread_p.cpp"
QStringList entries() const
bool copyTo(const QString &dest, bool recursive=true) const
const KArchiveEntry * entry(const QString &name) const
virtual bool isDirectory() const
QString name() const
virtual bool open(QIODevice::OpenMode mode)
const KArchiveDirectory * directory() const
object representing an installed package
Definition package.h:63
const QString path() const
Definition package.cpp:558
QString pluginId() const
bool value(const QString &key, bool defaultValue) const
static KPluginMetaData fromJsonFile(const QString &jsonFile)
QString version() const
QString i18n(const char *text, const TYPE &arg...)
void update(Part *part, const QByteArray &data, qint64 dataSize)
KIOCORE_EXPORT MimetypeJob * mimetype(const QUrl &url, JobFlags flags=DefaultFlags)
QString path(const QString &relativePath)
bool mkpath(const QString &dirPath) const const
QChar separator()
bool copy(const QString &fileName, const QString &newName)
bool exists() const const
QString baseName() const const
bool exists() const const
void append(QList< T > &&value)
qsizetype count() const const
void pop_back()
void push_back(parameter_type value)
QMimeType mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const const
bool inherits(const char *className) const const
int exitCode() const const
void setArguments(const QStringList &arguments)
void setProcessChannelMode(ProcessChannelMode mode)
void setProgram(const QString &program)
void start(OpenMode mode)
bool waitForFinished(int msecs)
virtual void run()=0
QString findExecutable(const QString &executableName, const QStringList &paths)
QString locate(StandardLocation type, const QString &fileName, LocateOptions options)
QString & append(QChar ch)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString join(QChar separator) const const
QString path() const const
void setAutoRemove(bool b)
QString scheme() const const
QString toString(FormattingOptions options) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:16:48 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.