1 | /* |
2 | This file is part of KNewStuff2. |
3 | SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org> |
4 | SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org> |
5 | |
6 | SPDX-License-Identifier: LGPL-2.1-or-later |
7 | */ |
8 | |
9 | #include "installation_p.h" |
10 | |
11 | #include <QDesktopServices> |
12 | #include <QDir> |
13 | #include <QFile> |
14 | #include <QProcess> |
15 | #include <QTemporaryFile> |
16 | #include <QUrlQuery> |
17 | |
18 | #include "karchive.h" |
19 | #include "knewstuff_version.h" |
20 | #include "qmimedatabase.h" |
21 | #include <KRandom> |
22 | #include <KShell> |
23 | #include <KTar> |
24 | #include <KZip> |
25 | |
26 | #include <KPackage/Package> |
27 | #include <KPackage/PackageJob> |
28 | |
29 | #include <KLocalizedString> |
30 | #include <knewstuffcore_debug.h> |
31 | #include <qstandardpaths.h> |
32 | |
33 | #include "jobs/filecopyjob.h" |
34 | #include "question.h" |
35 | #ifdef Q_OS_WIN |
36 | #include <shlobj.h> |
37 | #include <windows.h> |
38 | #endif |
39 | |
40 | using namespace KNSCore; |
41 | |
42 | Installation::Installation(QObject *parent) |
43 | : QObject(parent) |
44 | { |
45 | } |
46 | |
47 | bool Installation::readConfig(const KConfigGroup &group, QString &errorMessage) |
48 | { |
49 | // FIXME: add support for several categories later on |
50 | const QString uncompression = group.readEntry(key: "Uncompress" , QStringLiteral("never" )); |
51 | if (uncompression == QLatin1String("always" ) || uncompression == QLatin1String("true" )) { |
52 | uncompressSetting = AlwaysUncompress; |
53 | } else if (uncompression == QLatin1String("archive" )) { |
54 | uncompressSetting = UncompressIfArchive; |
55 | } else if (uncompression == QLatin1String("subdir" )) { |
56 | uncompressSetting = UncompressIntoSubdir; |
57 | } else if (uncompression == QLatin1String("kpackage" )) { |
58 | uncompressSetting = UseKPackageUncompression; |
59 | } else if (uncompression == QLatin1String("subdir-archive" )) { |
60 | uncompressSetting = UncompressIntoSubdirIfArchive; |
61 | } else if (uncompression == QLatin1String("never" )) { |
62 | uncompressSetting = NeverUncompress; |
63 | } else { |
64 | errorMessage = QStringLiteral("invalid Uncompress setting chosen, must be one of: subdir, always, archive, never, or kpackage" ); |
65 | qCCritical(KNEWSTUFFCORE) << errorMessage; |
66 | return false; |
67 | } |
68 | |
69 | kpackageStructure = group.readEntry(key: "KPackageStructure" ); |
70 | if (uncompressSetting == UseKPackageUncompression && kpackageStructure.isEmpty()) { |
71 | errorMessage = QStringLiteral("kpackage uncompress setting chosen, but no KPackageStructure specified" ); |
72 | qCCritical(KNEWSTUFFCORE) << errorMessage; |
73 | return false; |
74 | } |
75 | |
76 | postInstallationCommand = group.readEntry(key: "InstallationCommand" ); |
77 | uninstallCommand = group.readEntry(key: "UninstallCommand" ); |
78 | standardResourceDirectory = group.readEntry(key: "StandardResource" ); |
79 | targetDirectory = group.readEntry(key: "TargetDir" ); |
80 | xdgTargetDirectory = group.readEntry(key: "XdgTargetDir" ); |
81 | |
82 | installPath = group.readEntry(key: "InstallPath" ); |
83 | absoluteInstallPath = group.readEntry(key: "AbsoluteInstallPath" ); |
84 | |
85 | if (standardResourceDirectory.isEmpty() && targetDirectory.isEmpty() && xdgTargetDirectory.isEmpty() && installPath.isEmpty() |
86 | && absoluteInstallPath.isEmpty()) { |
87 | qCCritical(KNEWSTUFFCORE) << "No installation target set" ; |
88 | return false; |
89 | } |
90 | return true; |
91 | } |
92 | |
93 | void Installation::install(const Entry &entry) |
94 | { |
95 | downloadPayload(entry); |
96 | } |
97 | |
98 | void Installation::downloadPayload(const KNSCore::Entry &entry) |
99 | { |
100 | if (!entry.isValid()) { |
101 | Q_EMIT signalInstallationFailed(i18n("Invalid item." ), entry); |
102 | return; |
103 | } |
104 | QUrl source = QUrl(entry.payload()); |
105 | |
106 | if (!source.isValid()) { |
107 | qCCritical(KNEWSTUFFCORE) << "The entry doesn't have a payload." ; |
108 | Q_EMIT signalInstallationFailed(i18n("Download of item failed: no download URL for \"%1\"." , entry.name()), entry); |
109 | return; |
110 | } |
111 | |
112 | QString fileName(source.fileName()); |
113 | QTemporaryFile tempFile(QDir::tempPath() + QStringLiteral("/XXXXXX-" ) + fileName); |
114 | tempFile.setAutoRemove(false); |
115 | if (!tempFile.open()) { |
116 | return; // ERROR |
117 | } |
118 | QUrl destination = QUrl::fromLocalFile(localfile: tempFile.fileName()); |
119 | qCDebug(KNEWSTUFFCORE) << "Downloading payload" << source << "to" << destination; |
120 | #ifdef Q_OS_WIN // can't write to the file if it's open, on Windows |
121 | tempFile.close(); |
122 | #endif |
123 | |
124 | // FIXME: check for validity |
125 | FileCopyJob *job = FileCopyJob::file_copy(source, destination, permissions: -1, flags: JobFlag::Overwrite | JobFlag::HideProgressInfo); |
126 | connect(sender: job, signal: &KJob::result, context: this, slot: &Installation::slotPayloadResult); |
127 | |
128 | entry_jobs[job] = entry; |
129 | } |
130 | |
131 | void Installation::slotPayloadResult(KJob *job) |
132 | { |
133 | // for some reason this slot is getting called 3 times on one job error |
134 | if (entry_jobs.contains(key: job)) { |
135 | Entry entry = entry_jobs[job]; |
136 | entry_jobs.remove(key: job); |
137 | |
138 | if (job->error()) { |
139 | const QString errorMessage = i18n("Download of \"%1\" failed, error: %2" , entry.name(), job->errorString()); |
140 | qCWarning(KNEWSTUFFCORE) << errorMessage; |
141 | Q_EMIT signalInstallationFailed(message: errorMessage, entry); |
142 | } else { |
143 | FileCopyJob *fcjob = static_cast<FileCopyJob *>(job); |
144 | qCDebug(KNEWSTUFFCORE) << "Copied to" << fcjob->destUrl(); |
145 | QMimeDatabase db; |
146 | QMimeType mimeType = db.mimeTypeForFile(fileName: fcjob->destUrl().toLocalFile()); |
147 | if (mimeType.inherits(QStringLiteral("text/html" )) || mimeType.inherits(QStringLiteral("application/x-php" ))) { |
148 | const auto error = i18n("Cannot install '%1' because it points to a web page. Click <a href='%2'>here</a> to finish the installation." , |
149 | entry.name(), |
150 | fcjob->srcUrl().toString()); |
151 | Q_EMIT signalInstallationFailed(message: error, entry); |
152 | entry.setStatus(KNSCore::Entry::Invalid); |
153 | Q_EMIT signalEntryChanged(entry); |
154 | return; |
155 | } |
156 | |
157 | Q_EMIT signalPayloadLoaded(payload: fcjob->destUrl()); |
158 | install(entry, downloadedFile: fcjob->destUrl().toLocalFile()); |
159 | } |
160 | } |
161 | } |
162 | |
163 | void KNSCore::Installation::install(KNSCore::Entry entry, const QString &downloadedFile) |
164 | { |
165 | qCWarning(KNEWSTUFFCORE) << "Install:" << entry.name() << "from" << downloadedFile; |
166 | Q_ASSERT(QFileInfo::exists(downloadedFile)); |
167 | |
168 | if (entry.payload().isEmpty()) { |
169 | qCDebug(KNEWSTUFFCORE) << "No payload associated with:" << entry.name(); |
170 | return; |
171 | } |
172 | |
173 | // TODO Add async checksum verification |
174 | |
175 | QString targetPath = targetInstallationPath(); |
176 | QStringList installedFiles = installDownloadedFileAndUncompress(entry, payloadfile: downloadedFile, installdir: targetPath); |
177 | |
178 | if (uncompressionSetting() != UseKPackageUncompression) { |
179 | if (installedFiles.isEmpty()) { |
180 | if (entry.status() == KNSCore::Entry::Installing) { |
181 | entry.setStatus(KNSCore::Entry::Downloadable); |
182 | } else if (entry.status() == KNSCore::Entry::Updating) { |
183 | entry.setStatus(KNSCore::Entry::Updateable); |
184 | } |
185 | Q_EMIT signalEntryChanged(entry); |
186 | Q_EMIT signalInstallationFailed(i18n("Could not install \"%1\": file not found." , entry.name()), entry); |
187 | return; |
188 | } |
189 | |
190 | entry.setInstalledFiles(installedFiles); |
191 | |
192 | auto installationFinished = [this, entry]() { |
193 | Entry newentry = entry; |
194 | if (!newentry.updateVersion().isEmpty()) { |
195 | newentry.setVersion(newentry.updateVersion()); |
196 | } |
197 | if (newentry.updateReleaseDate().isValid()) { |
198 | newentry.setReleaseDate(newentry.updateReleaseDate()); |
199 | } |
200 | newentry.setStatus(KNSCore::Entry::Installed); |
201 | Q_EMIT signalEntryChanged(entry: newentry); |
202 | Q_EMIT signalInstallationFinished(entry: newentry); |
203 | }; |
204 | if (!postInstallationCommand.isEmpty()) { |
205 | QString scriptArgPath = !installedFiles.isEmpty() ? installedFiles.first() : targetPath; |
206 | if (scriptArgPath.endsWith(c: QLatin1Char('*'))) { |
207 | scriptArgPath = scriptArgPath.left(n: scriptArgPath.lastIndexOf(c: QLatin1Char('*'))); |
208 | } |
209 | QProcess *p = runPostInstallationCommand(installPath: scriptArgPath, entry); |
210 | connect(sender: p, signal: &QProcess::finished, context: this, slot: [entry, installationFinished, this](int exitCode) { |
211 | if (exitCode) { |
212 | Entry newEntry = entry; |
213 | newEntry.setStatus(KNSCore::Entry::Invalid); |
214 | Q_EMIT signalEntryChanged(entry: newEntry); |
215 | } else { |
216 | installationFinished(); |
217 | } |
218 | }); |
219 | } else { |
220 | installationFinished(); |
221 | } |
222 | } |
223 | } |
224 | |
225 | QString Installation::targetInstallationPath() const |
226 | { |
227 | // installdir is the target directory |
228 | QString installdir; |
229 | |
230 | const bool userScope = true; |
231 | // installpath also contains the file name if it's a single file, otherwise equal to installdir |
232 | int pathcounter = 0; |
233 | // wallpaper is already managed in the case of !xdgTargetDirectory.isEmpty() |
234 | if (!standardResourceDirectory.isEmpty() && standardResourceDirectory != QLatin1String("wallpaper" )) { |
235 | QStandardPaths::StandardLocation location = QStandardPaths::TempLocation; |
236 | // crude translation KStandardDirs names -> QStandardPaths enum |
237 | if (standardResourceDirectory == QLatin1String("tmp" )) { |
238 | location = QStandardPaths::TempLocation; |
239 | } else if (standardResourceDirectory == QLatin1String("config" )) { |
240 | location = QStandardPaths::ConfigLocation; |
241 | } |
242 | |
243 | if (userScope) { |
244 | installdir = QStandardPaths::writableLocation(type: location); |
245 | } else { // system scope |
246 | installdir = QStandardPaths::standardLocations(type: location).constLast(); |
247 | } |
248 | pathcounter++; |
249 | } |
250 | if (!targetDirectory.isEmpty() && targetDirectory != QLatin1String("/" )) { |
251 | if (userScope) { |
252 | installdir = QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QLatin1Char('/') + targetDirectory + QLatin1Char('/'); |
253 | } else { // system scope |
254 | installdir = QStandardPaths::locate(type: QStandardPaths::GenericDataLocation, fileName: targetDirectory, options: QStandardPaths::LocateDirectory) + QLatin1Char('/'); |
255 | } |
256 | pathcounter++; |
257 | } |
258 | if (!xdgTargetDirectory.isEmpty() && xdgTargetDirectory != QLatin1String("/" )) { |
259 | installdir = QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QLatin1Char('/') + xdgTargetDirectory + QLatin1Char('/'); |
260 | pathcounter++; |
261 | } |
262 | if (!installPath.isEmpty()) { |
263 | #if defined(Q_OS_WIN) |
264 | WCHAR wPath[MAX_PATH + 1]; |
265 | if (SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, wPath) == S_OK) { |
266 | installdir = QString::fromUtf16((const char16_t *)wPath) + QLatin1Char('/') + installPath + QLatin1Char('/'); |
267 | } else { |
268 | installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/'); |
269 | } |
270 | #else |
271 | installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/'); |
272 | #endif |
273 | pathcounter++; |
274 | } |
275 | if (!absoluteInstallPath.isEmpty()) { |
276 | installdir = absoluteInstallPath + QLatin1Char('/'); |
277 | pathcounter++; |
278 | } |
279 | |
280 | if (pathcounter != 1) { |
281 | qCCritical(KNEWSTUFFCORE) << "Wrong number of installation directories given." ; |
282 | return QString(); |
283 | } |
284 | |
285 | qCDebug(KNEWSTUFFCORE) << "installdir: " << installdir; |
286 | |
287 | // create the dir if it doesn't exist (QStandardPaths doesn't create it, unlike KStandardDirs!) |
288 | QDir().mkpath(dirPath: installdir); |
289 | |
290 | return installdir; |
291 | } |
292 | |
293 | QStringList Installation::installDownloadedFileAndUncompress(const KNSCore::Entry &entry, const QString &payloadfile, const QString installdir) |
294 | { |
295 | // Collect all files that were installed |
296 | QStringList installedFiles; |
297 | bool isarchive = true; |
298 | UncompressionOptions uncompressionOpt = uncompressionSetting(); |
299 | |
300 | // respect the uncompress flag in the knsrc |
301 | if (uncompressionOpt == UseKPackageUncompression) { |
302 | qCDebug(KNEWSTUFFCORE) << "Using KPackage for installation" ; |
303 | auto resetEntryStatus = [this, entry]() { |
304 | KNSCore::Entry changedEntry(entry); |
305 | if (changedEntry.status() == KNSCore::Entry::Installing || changedEntry.status() == KNSCore::Entry::Installed) { |
306 | changedEntry.setStatus(KNSCore::Entry::Downloadable); |
307 | } else if (changedEntry.status() == KNSCore::Entry::Updating) { |
308 | changedEntry.setStatus(KNSCore::Entry::Updateable); |
309 | } |
310 | Q_EMIT signalEntryChanged(entry: changedEntry); |
311 | }; |
312 | |
313 | qCDebug(KNEWSTUFFCORE) << "About to attempt to install" << payloadfile << "as" << kpackageStructure; |
314 | auto job = KPackage::PackageJob::install(packageFormat: kpackageStructure, sourcePackage: payloadfile); |
315 | connect(sender: job, signal: &KPackage::PackageJob::finished, context: this, slot: [this, entry, payloadfile, resetEntryStatus, job]() { |
316 | if (job->error() == KJob::NoError) { |
317 | Entry newentry = entry; |
318 | newentry.setInstalledFiles(QStringList{job->package().path()}); |
319 | // update version and release date to the new ones |
320 | if (newentry.status() == KNSCore::Entry::Updating) { |
321 | if (!newentry.updateVersion().isEmpty()) { |
322 | newentry.setVersion(newentry.updateVersion()); |
323 | } |
324 | if (newentry.updateReleaseDate().isValid()) { |
325 | newentry.setReleaseDate(newentry.updateReleaseDate()); |
326 | } |
327 | } |
328 | newentry.setStatus(KNSCore::Entry::Installed); |
329 | // We can remove the downloaded file, because we don't save its location and don't need it to uninstall the entry |
330 | QFile::remove(fileName: payloadfile); |
331 | Q_EMIT signalEntryChanged(entry: newentry); |
332 | Q_EMIT signalInstallationFinished(entry: newentry); |
333 | qCDebug(KNEWSTUFFCORE) << "Install job finished with no error and we now have files" << job->package().path(); |
334 | } else { |
335 | if (job->error() == KPackage::PackageJob::JobError::NewerVersionAlreadyInstalledError) { |
336 | Entry newentry = entry; |
337 | newentry.setStatus(KNSCore::Entry::Installed); |
338 | newentry.setInstalledFiles(QStringList{job->package().path()}); |
339 | // update version and release date to the new ones |
340 | if (!newentry.updateVersion().isEmpty()) { |
341 | newentry.setVersion(newentry.updateVersion()); |
342 | } |
343 | if (newentry.updateReleaseDate().isValid()) { |
344 | newentry.setReleaseDate(newentry.updateReleaseDate()); |
345 | } |
346 | Q_EMIT signalEntryChanged(entry: newentry); |
347 | Q_EMIT signalInstallationFinished(entry: newentry); |
348 | qCDebug(KNEWSTUFFCORE) << "Install job finished telling us this item was already installed with this version, so... let's " |
349 | "just make a small fib and say we totally installed that, honest, and we now have files" |
350 | << job->package().path(); |
351 | } else { |
352 | Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2" , payloadfile, job->errorText()), entry); |
353 | resetEntryStatus(); |
354 | qCDebug(KNEWSTUFFCORE) << "Install job finished with error state" << job->error() << "and description" << job->error(); |
355 | } |
356 | } |
357 | }); |
358 | } else { |
359 | if (uncompressionOpt == AlwaysUncompress || uncompressionOpt == UncompressIntoSubdirIfArchive || uncompressionOpt == UncompressIfArchive |
360 | || uncompressionOpt == UncompressIntoSubdir) { |
361 | // this is weird but a decompression is not a single name, so take the path instead |
362 | QMimeDatabase db; |
363 | QMimeType mimeType = db.mimeTypeForFile(fileName: payloadfile); |
364 | qCDebug(KNEWSTUFFCORE) << "Postinstallation: uncompress the file" ; |
365 | |
366 | // FIXME: check for overwriting, malicious archive entries (../foo) etc. |
367 | // FIXME: KArchive should provide "safe mode" for this! |
368 | QScopedPointer<KArchive> archive; |
369 | |
370 | if (mimeType.inherits(QStringLiteral("application/zip" ))) { |
371 | archive.reset(other: new KZip(payloadfile)); |
372 | // clang-format off |
373 | } else if (mimeType.inherits(QStringLiteral("application/tar" )) |
374 | || mimeType.inherits(QStringLiteral("application/x-tar" )) // BUG 450662 |
375 | || mimeType.inherits(QStringLiteral("application/x-gzip" )) |
376 | || mimeType.inherits(QStringLiteral("application/x-bzip" )) |
377 | || mimeType.inherits(QStringLiteral("application/x-lzma" )) |
378 | || mimeType.inherits(QStringLiteral("application/x-xz" )) |
379 | || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar" )) |
380 | || mimeType.inherits(QStringLiteral("application/x-compressed-tar" ))) { |
381 | // clang-format on |
382 | archive.reset(other: new KTar(payloadfile)); |
383 | } else { |
384 | qCCritical(KNEWSTUFFCORE) << "Could not determine type of archive file" << payloadfile; |
385 | if (uncompressionOpt == AlwaysUncompress) { |
386 | Q_EMIT signalInstallationError(i18n("Could not determine the type of archive of the downloaded file %1" , payloadfile), entry); |
387 | return QStringList(); |
388 | } |
389 | isarchive = false; |
390 | } |
391 | |
392 | if (isarchive) { |
393 | bool success = archive->open(mode: QIODevice::ReadOnly); |
394 | if (!success) { |
395 | qCCritical(KNEWSTUFFCORE) << "Cannot open archive file" << payloadfile; |
396 | if (uncompressionOpt == AlwaysUncompress) { |
397 | Q_EMIT signalInstallationError( |
398 | i18n("Failed to open the archive file %1. The reported error was: %2" , payloadfile, archive->errorString()), |
399 | entry); |
400 | return QStringList(); |
401 | } |
402 | // otherwise, just copy the file |
403 | isarchive = false; |
404 | } |
405 | |
406 | if (isarchive) { |
407 | const KArchiveDirectory *dir = archive->directory(); |
408 | // if there is more than an item in the file, and we are requested to do so |
409 | // put contents in a subdirectory with the same name as the file |
410 | QString installpath; |
411 | const bool isSubdir = |
412 | (uncompressionOpt == UncompressIntoSubdir || uncompressionOpt == UncompressIntoSubdirIfArchive) && dir->entries().count() > 1; |
413 | if (isSubdir) { |
414 | installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName(); |
415 | } else { |
416 | installpath = installdir; |
417 | } |
418 | |
419 | if (dir->copyTo(dest: installpath)) { |
420 | // If we extracted the subdir we want to save it using the /* notation like we would when using the "archive" option |
421 | // Also if we use an (un)install command we only call it once with the folder as argument and not for each file |
422 | if (isSubdir) { |
423 | installedFiles << QDir(installpath).absolutePath() + QLatin1String("/*" ); |
424 | } else { |
425 | installedFiles << archiveEntries(path: installpath, dir); |
426 | } |
427 | } else { |
428 | qCWarning(KNEWSTUFFCORE) << "could not install" << entry.name() << "to" << installpath; |
429 | } |
430 | |
431 | archive->close(); |
432 | QFile::remove(fileName: payloadfile); |
433 | } |
434 | } |
435 | } |
436 | |
437 | qCDebug(KNEWSTUFFCORE) << "isarchive:" << isarchive; |
438 | |
439 | // some wallpapers are compressed, some aren't |
440 | if ((!isarchive && standardResourceDirectory == QLatin1String("wallpaper" )) |
441 | || (uncompressionOpt == NeverUncompress || (uncompressionOpt == UncompressIfArchive && !isarchive) |
442 | || (uncompressionOpt == UncompressIntoSubdirIfArchive && !isarchive))) { |
443 | // no decompress but move to target |
444 | |
445 | /// @todo when using KIO::get the http header can be accessed and it contains a real file name. |
446 | // FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names |
447 | QUrl source = QUrl(entry.payload()); |
448 | qCDebug(KNEWSTUFFCORE) << "installing non-archive from" << source; |
449 | const QString installpath = QDir(installdir).filePath(fileName: source.fileName()); |
450 | |
451 | qCDebug(KNEWSTUFFCORE) << "Install to file" << installpath; |
452 | // FIXME: copy goes here (including overwrite checking) |
453 | // FIXME: what must be done now is to update the cache *again* |
454 | // in order to set the new payload filename (on root tag only) |
455 | // - this might or might not need to take uncompression into account |
456 | // FIXME: for updates, we might need to force an overwrite (that is, deleting before) |
457 | QFile file(payloadfile); |
458 | bool success = true; |
459 | const bool update = ((entry.status() == KNSCore::Entry::Updateable) || (entry.status() == KNSCore::Entry::Updating)); |
460 | |
461 | if (QFile::exists(fileName: installpath) && QDir::tempPath() != installdir) { |
462 | if (!update) { |
463 | Question question(Question::ContinueCancelQuestion); |
464 | question.setEntry(entry); |
465 | question.setQuestion(i18n("This file already exists on disk (possibly due to an earlier failed download attempt). Continuing means " |
466 | "overwriting it. Do you wish to overwrite the existing file?" ) |
467 | + QStringLiteral("\n'" ) + installpath + QLatin1Char('\'')); |
468 | question.setTitle(i18n("Overwrite File" )); |
469 | if (question.ask() != Question::ContinueResponse) { |
470 | return QStringList(); |
471 | } |
472 | } |
473 | success = QFile::remove(fileName: installpath); |
474 | } |
475 | if (success) { |
476 | // remove in case it's already present and in a temporary directory, so we get to actually use the path again |
477 | if (installpath.startsWith(s: QDir::tempPath())) { |
478 | QFile::remove(fileName: installpath); |
479 | } |
480 | success = file.rename(newName: installpath); |
481 | qCWarning(KNEWSTUFFCORE) << "move:" << file.fileName() << "to" << installpath; |
482 | if (!success) { |
483 | qCWarning(KNEWSTUFFCORE) << file.errorString(); |
484 | } |
485 | } |
486 | if (!success) { |
487 | Q_EMIT signalInstallationError(i18n("Unable to move the file %1 to the intended destination %2" , payloadfile, installpath), entry); |
488 | qCCritical(KNEWSTUFFCORE) << "Cannot move file" << payloadfile << "to destination" << installpath; |
489 | return QStringList(); |
490 | } |
491 | installedFiles << installpath; |
492 | } |
493 | } |
494 | |
495 | return installedFiles; |
496 | } |
497 | |
498 | QProcess *Installation::runPostInstallationCommand(const QString &installPath, const KNSCore::Entry &entry) |
499 | { |
500 | QString command(postInstallationCommand); |
501 | QString fileArg(KShell::quoteArg(arg: installPath)); |
502 | command.replace(before: QLatin1String("%f" ), after: fileArg); |
503 | |
504 | qCDebug(KNEWSTUFFCORE) << "Run command:" << command; |
505 | |
506 | QProcess *ret = new QProcess(this); |
507 | auto onProcessFinished = [this, command, ret, entry](int exitcode, QProcess::ExitStatus status) { |
508 | const QString output{QString::fromLocal8Bit(ba: ret->readAllStandardError())}; |
509 | if (status == QProcess::CrashExit) { |
510 | QString errorMessage = i18n("The installation failed while attempting to run the command:\n%1\n\nThe returned output was:\n%2" , command, output); |
511 | Q_EMIT signalInstallationError(message: errorMessage, entry); |
512 | qCCritical(KNEWSTUFFCORE) << "Process crashed with command:" << command; |
513 | } else if (exitcode) { |
514 | // 130 means Ctrl+C as an exit code this is interpreted by KNewStuff as cancel operation |
515 | // and no error will be displayed to the user, BUG: 436355 |
516 | if (exitcode == 130) { |
517 | qCCritical(KNEWSTUFFCORE) << "Command" << command << "failed was aborted by the user" ; |
518 | Q_EMIT signalInstallationFinished(entry); |
519 | } else { |
520 | Q_EMIT signalInstallationError( |
521 | i18n("The installation failed with code %1 while attempting to run the command:\n%2\n\nThe returned output was:\n%3" , |
522 | exitcode, |
523 | command, |
524 | output), |
525 | entry); |
526 | qCCritical(KNEWSTUFFCORE) << "Command" << command << "failed with code" << exitcode; |
527 | } |
528 | } |
529 | sender()->deleteLater(); |
530 | }; |
531 | connect(sender: ret, signal: &QProcess::finished, context: this, slot&: onProcessFinished); |
532 | |
533 | QStringList args = KShell::splitArgs(cmd: command); |
534 | ret->setProgram(args.takeFirst()); |
535 | ret->setArguments(args); |
536 | ret->start(); |
537 | return ret; |
538 | } |
539 | |
540 | void Installation::uninstall(Entry entry) |
541 | { |
542 | const auto deleteFilesAndMarkAsUninstalled = [entry, this]() { |
543 | bool deletionSuccessful = true; |
544 | const auto lst = entry.installedFiles(); |
545 | for (const QString &file : lst) { |
546 | // This is used to delete the download location if there are no more entries |
547 | QFileInfo info(file); |
548 | if (info.isDir()) { |
549 | QDir().rmdir(dirName: file); |
550 | } else if (file.endsWith(s: QLatin1String("/*" ))) { |
551 | QDir dir(file.left(n: file.size() - 2)); |
552 | bool worked = dir.removeRecursively(); |
553 | if (!worked) { |
554 | qCWarning(KNEWSTUFFCORE) << "Couldn't remove" << dir.path(); |
555 | continue; |
556 | } |
557 | } else { |
558 | if (info.exists() || info.isSymLink()) { |
559 | bool worked = QFile::remove(fileName: file); |
560 | if (!worked) { |
561 | qWarning() << "unable to delete file " << file; |
562 | Q_EMIT signalInstallationFailed( |
563 | i18n("The removal of %1 failed, as the installed file %2 could not be automatically removed. You can attempt to manually delete " |
564 | "this file, if you believe this is an error." , |
565 | entry.name(), |
566 | file), |
567 | entry); |
568 | // Assume that the uninstallation has failed, and reset the entry to an installed state |
569 | deletionSuccessful = false; |
570 | break; |
571 | } |
572 | } else { |
573 | qWarning() << "unable to delete file " << file << ". file does not exist." ; |
574 | } |
575 | } |
576 | } |
577 | Entry newEntry = entry; |
578 | if (deletionSuccessful) { |
579 | newEntry.setEntryDeleted(); |
580 | } else { |
581 | newEntry.setStatus(KNSCore::Entry::Installed); |
582 | } |
583 | |
584 | Q_EMIT signalEntryChanged(entry: newEntry); |
585 | }; |
586 | |
587 | if (uncompressionSetting() == UseKPackageUncompression) { |
588 | const auto lst = entry.installedFiles(); |
589 | if (lst.length() == 1) { |
590 | const QString installedFile{lst.first()}; |
591 | |
592 | KJob *job = KPackage::PackageJob::uninstall(packageFormat: kpackageStructure, pluginId: installedFile); |
593 | connect(sender: job, signal: &KJob::result, context: this, slot: [this, installedFile, entry, job]() { |
594 | Entry newEntry = entry; |
595 | if (job->error() == KJob::NoError) { |
596 | newEntry.setEntryDeleted(); |
597 | Q_EMIT signalEntryChanged(entry: newEntry); |
598 | } else { |
599 | Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2" , installedFile, job->errorText()), entry); |
600 | } |
601 | }); |
602 | } |
603 | deleteFilesAndMarkAsUninstalled(); |
604 | } else { |
605 | const auto lst = entry.installedFiles(); |
606 | // If there is an uninstall script, make sure it runs without errors |
607 | if (!uninstallCommand.isEmpty()) { |
608 | bool validFileExisted = false; |
609 | for (const QString &file : lst) { |
610 | QString filePath = file; |
611 | bool validFile = QFileInfo::exists(file: filePath); |
612 | // If we have uncompressed a subdir we write <path>/* in the config, but when calling a script |
613 | // we want to convert this to a normal path |
614 | if (!validFile && file.endsWith(c: QLatin1Char('*'))) { |
615 | filePath = filePath.left(n: filePath.lastIndexOf(c: QLatin1Char('*'))); |
616 | validFile = QFileInfo::exists(file: filePath); |
617 | } |
618 | if (validFile) { |
619 | validFileExisted = true; |
620 | QString fileArg(KShell::quoteArg(arg: filePath)); |
621 | QString command(uninstallCommand); |
622 | command.replace(before: QLatin1String("%f" ), after: fileArg); |
623 | |
624 | QStringList args = KShell::splitArgs(cmd: command); |
625 | const QString program = args.takeFirst(); |
626 | QProcess *process = new QProcess(this); |
627 | process->start(program, arguments: args); |
628 | auto onProcessFinished = [this, command, process, entry, deleteFilesAndMarkAsUninstalled](int, QProcess::ExitStatus status) { |
629 | if (status == QProcess::CrashExit) { |
630 | const QString processOutput = QString::fromLocal8Bit(ba: process->readAllStandardError()); |
631 | const QString err = i18n( |
632 | "The uninstallation process failed to successfully run the command %1\n" |
633 | "The output of was: \n%2\n" |
634 | "If you think this is incorrect, you can continue or cancel the uninstallation process" , |
635 | KShell::quoteArg(command), |
636 | processOutput); |
637 | Q_EMIT signalInstallationError(message: err, entry); |
638 | // Ask the user if he wants to continue, even though the script failed |
639 | Question question(Question::ContinueCancelQuestion); |
640 | question.setEntry(entry); |
641 | question.setQuestion(err); |
642 | Question::Response response = question.ask(); |
643 | if (response == Question::CancelResponse) { |
644 | // Use can delete files manually |
645 | Entry newEntry = entry; |
646 | newEntry.setStatus(KNSCore::Entry::Installed); |
647 | Q_EMIT signalEntryChanged(entry: newEntry); |
648 | return; |
649 | } |
650 | } else { |
651 | qCDebug(KNEWSTUFFCORE) << "Command executed successfully:" << command; |
652 | } |
653 | deleteFilesAndMarkAsUninstalled(); |
654 | }; |
655 | connect(sender: process, signal: &QProcess::finished, context: this, slot&: onProcessFinished); |
656 | } |
657 | } |
658 | // If the entry got deleted, but the RemoveDeadEntries option was not selected this case can happen |
659 | if (!validFileExisted) { |
660 | deleteFilesAndMarkAsUninstalled(); |
661 | } |
662 | } else { |
663 | deleteFilesAndMarkAsUninstalled(); |
664 | } |
665 | } |
666 | } |
667 | |
668 | Installation::UncompressionOptions Installation::uncompressionSetting() const |
669 | { |
670 | return uncompressSetting; |
671 | } |
672 | |
673 | QStringList Installation::archiveEntries(const QString &path, const KArchiveDirectory *dir) |
674 | { |
675 | QStringList files; |
676 | const auto lst = dir->entries(); |
677 | for (const QString &entry : lst) { |
678 | const auto currentEntry = dir->entry(name: entry); |
679 | |
680 | const QString childPath = QDir(path).filePath(fileName: entry); |
681 | if (currentEntry->isFile()) { |
682 | files << childPath; |
683 | } else if (currentEntry->isDirectory()) { |
684 | files << childPath + QStringLiteral("/*" ); |
685 | } |
686 | } |
687 | return files; |
688 | } |
689 | |
690 | #include "moc_installation_p.cpp" |
691 | |