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 | |
32 | namespace KPackage |
33 | { |
34 | bool 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 | |
71 | bool removeFolder(QString folderPath) |
72 | { |
73 | QDir folder(folderPath); |
74 | return folder.removeRecursively(); |
75 | } |
76 | |
77 | class PackageJobThreadPrivate |
78 | { |
79 | public: |
80 | QString installPath; |
81 | QString errorMessage; |
82 | std::function<void()> run; |
83 | int errorCode; |
84 | }; |
85 | |
86 | PackageJobThread::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 | |
111 | PackageJobThread::~PackageJobThread() |
112 | { |
113 | delete d; |
114 | } |
115 | |
116 | void PackageJobThread::run() |
117 | { |
118 | Q_ASSERT(d->run); |
119 | d->run(); |
120 | } |
121 | bool 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 | |
129 | static 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 | |
141 | bool 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 | |
158 | bool 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 | |
348 | bool 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 | |
356 | bool 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 | |
365 | bool 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 | |
396 | PackageJob::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 | |