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(dirName: targetName);
46 target = QDir(targetPath);
47 }
48
49 const auto lstEntries = source.entryList(filters: 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(fileName: sourceFilePath, newName: targetFilePath)) {
55 return false;
56 }
57 }
58 const auto lstEntries2 = source.entryList(filters: 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(sourcePath: sourceSubFolderPath, targetPath: 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, operation: PackageJob::Install);
124 Q_EMIT installPathChanged(installPath: d->installPath);
125 Q_EMIT jobThreadFinished(success: ok, errorCode: errorCode(), errorMessage: d->errorMessage);
126 return ok;
127}
128
129static QString resolveHandler(const QString &scheme)
130{
131 QString envOverride = qEnvironmentVariable(varName: "KPACKAGE_DEP_RESOLVERS_PATH");
132 QStringList searchDirs;
133 if (!envOverride.isEmpty()) {
134 searchDirs.push_back(t: 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(executableName: scheme + QLatin1String("handler"), paths: searchDirs);
139}
140
141bool PackageJobThread::installDependency(const QUrl &destUrl)
142{
143 auto handler = resolveHandler(scheme: destUrl.scheme());
144 if (handler.isEmpty()) {
145 return false;
146 }
147
148 QProcess process;
149 process.setProgram(handler);
150 process.setArguments({destUrl.toString()});
151 process.setProcessChannelMode(QProcess::ForwardedChannels);
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(dirPath: 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
178 QString path;
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(c: QLatin1Char('/'))) {
187 path.append(c: QLatin1Char('/'));
188 }
189 } else {
190 KArchive *archive = nullptr;
191 QMimeDatabase db;
192 QMimeType mimetype = db.mimeTypeForFile(fileName: src);
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(mode: 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(dest: path);
223
224 QStringList entries = source->entries();
225 if (entries.count() == 1) {
226 const KArchiveEntry *entry = source->entry(name: 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(subject: 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(c: QLatin1Char('/'));
266 }
267 targetName.append(s: pluginName);
268
269 if (QFile::exists(fileName: targetName)) {
270 if (operation == PackageJob::Update) {
271 KPluginMetaData oldMeta;
272 if (QString jsonPath = targetName + QLatin1String("/metadata.json"); QFileInfo::exists(file: jsonPath)) {
273 oldMeta = KPluginMetaData::fromJsonFile(jsonFile: jsonPath);
274 }
275
276 if (readKPackageType(metaData: oldMeta) != readKPackageType(metaData: 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(version1: oldMeta.version(), version2: meta.version())) {
280 const bool ok = uninstallPackage(packagePath: 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"), defaultValue: QStringList());
303 for (const QString &dep : dependencies) {
304 QUrl depUrl(dep);
305 const QString knsrcFilePath = QStandardPaths::locate(type: QStandardPaths::GenericDataLocation, fileName: QLatin1String("knsrcfiles/") + depUrl.host());
306 if (knsrcFilePath.isEmpty() && optionalDependencies.contains(str: depUrl.host())) {
307 qWarning() << "Skipping depdendency due to knsrc files being missing" << depUrl;
308 continue;
309 }
310 if (!installDependency(destUrl: 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(sourcePath: path, targetPath: targetName);
320 removeFolder(folderPath: 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(sourcePath: path, targetPath: 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, operation: PackageJob::Update);
351 Q_EMIT installPathChanged(installPath: d->installPath);
352 Q_EMIT jobThreadFinished(success: ok, errorCode: errorCode(), errorMessage: 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(success: ok, errorCode: errorCode(), errorMessage: d->errorMessage);
362 return ok;
363}
364
365bool PackageJobThread::uninstallPackage(const QString &packagePath)
366{
367 if (!QFile::exists(fileName: 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(sep: QLatin1Char('/'));
377 int ix = ps.count() - 1;
378 if (packagePath.endsWith(c: QLatin1Char('/'))) {
379 ix = ps.count() - 2;
380 }
381 pkg = ps[ix];
382 ps.pop_back();
383 root = ps.join(sep: QLatin1Char('/'));
384 }
385
386 bool ok = removeFolder(folderPath: 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"
404

source code of kpackage/src/kpackage/private/packagejobthread.cpp