KNewStuff

installation.cpp
1 /*
2  This file is part of KNewStuff2.
3  SPDX-FileCopyrightText: 2007 Josef Spillner <[email protected]>
4  SPDX-FileCopyrightText: 2009 Frederik Gladhorn <[email protected]>
5 
6  SPDX-License-Identifier: LGPL-2.1-or-later
7 */
8 
9 #include "installation.h"
10 
11 #include <QDir>
12 #include <QFile>
13 #include <QTemporaryFile>
14 #include <QProcess>
15 #include <QUrlQuery>
16 #include <QDesktopServices>
17 
18 #include "qmimedatabase.h"
19 #include "karchive.h"
20 #include <KZip>
21 #include <KTar>
22 #include <KRandom>
23 #include <KShell>
24 
25 #include <KPackage/PackageStructure>
26 #include <KPackage/Package>
27 #include <KPackage/PackageLoader>
28 #include "jobs/kpackagejob.h"
29 
30 #include <qstandardpaths.h>
31 #include <KLocalizedString>
32 #include <knewstuffcore_debug.h>
33 
34 #include "jobs/filecopyjob.h"
35 #include "question.h"
36 #ifdef Q_OS_WIN
37 #include <windows.h>
38 #include <shlobj.h>
39 #endif
40 
41 using namespace KNSCore;
42 
44  : QObject(parent)
45 {
46  // TODO KF6 Make these real properties, when we can refactor this and add a proper dptr
47  setProperty("kpackageType", QLatin1String(""));
48  setProperty("uncompressSetting", UncompressionOptions::NeverUncompress);
49 }
50 
51 bool Installation::readConfig(const KConfigGroup &group)
52 {
53  // FIXME: add support for several categories later on
54  // FIXME: read out only when actually installing as a performance improvement?
55  uncompression = group.readEntry("Uncompress", QStringLiteral("never"));
57  // support old value of true as equivalent of always
58  if (uncompression == QLatin1String("true")) {
59  uncompression = QStringLiteral("always");
60  }
61  if (uncompression == QLatin1String("always")) {
62  opt = AlwaysUncompress;
63  } else if (uncompression == QLatin1String("archive")) {
64  opt = UncompressIfArchive;
65  } else if (uncompression == QLatin1String("subdir")) {
67  } else if (uncompression == QLatin1String("kpackage")) {
69  } else if (uncompression == QLatin1String("subdir-archive")) {
71  } else if (uncompression == QLatin1String("never")) {
72  opt = NeverUncompress;
73  } else {
74  qCCritical(KNEWSTUFFCORE) << "invalid Uncompress setting chosen, must be one of: subdir, always, archive, never, or kpackage";
75  return false;
76  }
77  setProperty("uncompressSetting", opt);
78  setProperty("kpackageType", group.readEntry("KPackageType"));
79  postInstallationCommand = group.readEntry("InstallationCommand");
80  uninstallCommand = group.readEntry("UninstallCommand");
81  standardResourceDirectory = group.readEntry("StandardResource");
82  targetDirectory = group.readEntry("TargetDir");
83  xdgTargetDirectory = group.readEntry("XdgTargetDir");
84 
85 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 77)
86  // Provide some compatibility
87  if (standardResourceDirectory == QLatin1String("wallpaper")) {
88  xdgTargetDirectory = QStringLiteral("wallpapers");
89  }
90 
91  // also, ensure wallpapers are decompressed into subdirectories
92  // this ensures that wallpapers with multiple resolutions continue to function
93  // as expected
94  if (xdgTargetDirectory == QLatin1String("wallpapers")) {
95  uncompression = QStringLiteral("subdir");
96  }
97 
98  // A touch of special treatment for the various old kpackage based knsrc files, so they work
99  // with the new, internal stuff. The result unfortunately is that all entries marked as
100  // installed in knewstuff no longer will be, but since it never worked right anyway... we'll
101  // simply have to live with that.
102  if (postInstallationCommand.startsWith(QLatin1String("kpackagetool5 -t")) &&
103  postInstallationCommand.endsWith(QLatin1String("-i %f")) &&
104  uninstallCommand.startsWith(QLatin1String("kpackagetool5 -t")) &&
105  uninstallCommand.endsWith(QLatin1String("-r %f"))) {
106  uncompression = QStringLiteral("kpackage");
107  postInstallationCommand = QLatin1String("");
108  // Not clearing uninstallCommand, as this is used for the fallback situation
109  setProperty("kpackageType", uninstallCommand.mid(17, uninstallCommand.length() - 17 - 6));
110  qCWarning(KNEWSTUFFCORE) << "Your configuration file uses an old version of the kpackage support, and should be converted. Please report this to the author of the software you are currently using. The package type, we assume, is" << property("kpackageType").toString();
111  } else if (postInstallationCommand.startsWith(QLatin1String("kpackagetool5 --type")) &&
112  postInstallationCommand.endsWith(QLatin1String("--install %f")) &&
113  uninstallCommand.startsWith(QLatin1String("kpackagetool5 --type")) &&
114  uninstallCommand.endsWith(QLatin1String("--remove %f"))) {
115  uncompression = QStringLiteral("kpackage");
116  postInstallationCommand = QLatin1String("");
117  // Not clearing uninstallCommand, as this is used for the fallback situation
118  setProperty("kpackageType", uninstallCommand.mid(21, uninstallCommand.length() - 21 - 12));
119  qCWarning(KNEWSTUFFCORE) << "Your configuration file uses an old version of the kpackage support, and should be converted. Please report this to the author of the software you are currently using. The package type, we assume, is" << property("kpackageType").toString();
120  }
121 #endif
122 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 79)
123  customName = group.readEntry("CustomName", false);
124  if (customName) {
125  qWarning(KNEWSTUFFCORE) << "The CustomName property is deprecated and will be removed in KF6";
126  }
127  QString scopeString = group.readEntry("Scope");
128  if (!scopeString.isEmpty()) {
129  qWarning(KNEWSTUFFCORE) << "Setting the scope is deprecated, it will default to user";
130  if (scopeString == QLatin1String("user")) {
131  scope = ScopeUser;
132  } else if (scopeString == QLatin1String("system")) {
133  scope = ScopeSystem;
134  } else {
135  qCCritical(KNEWSTUFFCORE) << QStringLiteral("The scope '") + scopeString + QStringLiteral("' is unknown.");
136  return false;
137  }
138 
139  if (scope == ScopeSystem) {
140  if (!installPath.isEmpty()) {
141  qCCritical(KNEWSTUFFCORE) << "System installation cannot be mixed with InstallPath.";
142  return false;
143  }
144  }
145  }
146 
147  QString checksumpolicy = group.readEntry("ChecksumPolicy");
148  if (!checksumpolicy.isEmpty()) {
149  qWarning(KNEWSTUFFCORE) << "The ChecksumPolicy feature is defunct";
150  if (checksumpolicy == QLatin1String("never")) {
151  checksumPolicy = Installation::CheckNever;
152  } else if (checksumpolicy == QLatin1String("ifpossible")) {
153  checksumPolicy = Installation::CheckIfPossible;
154  } else if (checksumpolicy == QLatin1String("always")) {
155  checksumPolicy = Installation::CheckAlways;
156  } else {
157  qCCritical(KNEWSTUFFCORE) << QStringLiteral("The checksum policy '") + checksumpolicy + QStringLiteral("' is unknown.");
158  return false;
159  }
160  }
161 
162  QString signaturepolicy = group.readEntry("SignaturePolicy");
163  if (!signaturepolicy.isEmpty()) {
164  qWarning(KNEWSTUFFCORE) << "The SignaturePolicy feature is defunct";
165  if (signaturepolicy == QLatin1String("never")) {
166  signaturePolicy = Installation::CheckNever;
167  } else if (signaturepolicy == QLatin1String("ifpossible")) {
168  signaturePolicy = Installation::CheckIfPossible;
169  } else if (signaturepolicy == QLatin1String("always")) {
170  signaturePolicy = Installation::CheckAlways;
171  } else {
172  qCCritical(KNEWSTUFFCORE) << QStringLiteral("The signature policy '") + signaturepolicy + QStringLiteral("' is unknown.");
173  return false;
174  }
175  }
176  acceptHtml = group.readEntry("AcceptHtmlDownloads", false);
177  if (acceptHtml) {
178  qWarning(KNEWSTUFFCORE) << "The AcceptHtmlDownload property is deprecated and will default to false. If there"
179  "is a HTML download link the user will be prompted if the installation should proceed";
180  }
181 #endif
182 
183  installPath = group.readEntry("InstallPath");
184  absoluteInstallPath = group.readEntry("AbsoluteInstallPath");
185 
186  if (standardResourceDirectory.isEmpty() &&
187  targetDirectory.isEmpty() &&
188  xdgTargetDirectory.isEmpty() &&
189  installPath.isEmpty() &&
190  absoluteInstallPath.isEmpty()) {
191  qCCritical(KNEWSTUFFCORE) << "No installation target set";
192  return false;
193  }
194  return true;
195 }
196 
197 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 71)
198 bool Installation::isRemote() const
199 {
200  return false;
201 }
202 #endif
203 
205 {
206  downloadPayload(entry);
207 }
208 
210 {
211  if (!entry.isValid()) {
212  Q_EMIT signalInstallationFailed(i18n("Invalid item."));
213  return;
214  }
215  QUrl source = QUrl(entry.payload());
216 
217  if (!source.isValid()) {
218  qCCritical(KNEWSTUFFCORE) << "The entry doesn't have a payload.";
219  Q_EMIT signalInstallationFailed(i18n("Download of item failed: no download URL for \"%1\".", entry.name()));
220  return;
221  }
222 
223  QString fileName(source.fileName());
224  QTemporaryFile tempFile(QDir::tempPath() + QStringLiteral("/XXXXXX-") + fileName);
225  if (!tempFile.open()) {
226  return; // ERROR
227  }
228  QUrl destination = QUrl::fromLocalFile(tempFile.fileName());
229  qCDebug(KNEWSTUFFCORE) << "Downloading payload" << source << "to" << destination;
230 
231  // FIXME: check for validity
232  FileCopyJob *job = FileCopyJob::file_copy(source, destination, -1, JobFlag::Overwrite | JobFlag::HideProgressInfo);
233  connect(job,
234  &KJob::result,
235  this, &Installation::slotPayloadResult);
236 
237  entry_jobs[job] = entry;
238 }
239 
240 void Installation::slotPayloadResult(KJob *job)
241 {
242  // for some reason this slot is getting called 3 times on one job error
243  if (entry_jobs.contains(job)) {
244  EntryInternal entry = entry_jobs[job];
245  entry_jobs.remove(job);
246 
247  if (job->error()) {
248  Q_EMIT signalInstallationFailed(i18n("Download of \"%1\" failed, error: %2", entry.name(), job->errorString()));
249  } else {
250  FileCopyJob *fcjob = static_cast<FileCopyJob *>(job);
251 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 79)
252  // check if the app likes html files - disabled by default as too many bad links have been submitted to opendesktop.org
253  if (!acceptHtml) {
254 #endif
255  QMimeDatabase db;
256  QMimeType mimeType = db.mimeTypeForFile(fcjob->destUrl().toLocalFile());
257  if (mimeType.inherits(QStringLiteral("text/html")) || mimeType.inherits(QStringLiteral("application/x-php"))) {
258  Question question;
259  question.setQuestion(i18n("The downloaded file is a html file. This indicates a link to a website instead of the actual download. Would you like to open the site with a browser instead?"));
260  question.setTitle(i18n("Possibly bad download link"));
261  if(question.ask() == Question::YesResponse) {
262  QDesktopServices::openUrl(fcjob->srcUrl());
263  Q_EMIT signalInstallationFailed(i18n("Downloaded file was a HTML file. Opened in browser."));
264  entry.setStatus(KNS3::Entry::Invalid);
265  Q_EMIT signalEntryChanged(entry);
266  return;
267  }
268  }
269 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 79)
270  }
271 #endif
272 
273  Q_EMIT signalPayloadLoaded(fcjob->destUrl());
274  install(entry, fcjob->destUrl().toLocalFile());
275  }
276  }
277 }
278 
279 void KNSCore::Installation::install(KNSCore::EntryInternal entry, const QString& downloadedFile)
280 {
281  qCDebug(KNEWSTUFFCORE) << "Install: " << entry.name() << " from " << downloadedFile;
282 
283  if (entry.payload().isEmpty()) {
284  qCDebug(KNEWSTUFFCORE) << "No payload associated with: " << entry.name();
285  return;
286  }
287 
288  // TODO Add async checksum verification
289 
290  QString targetPath = targetInstallationPath();
291  QStringList installedFiles = installDownloadedFileAndUncompress(entry, downloadedFile, targetPath);
292 
294  if (installedFiles.isEmpty()) {
295  if (entry.status() == KNS3::Entry::Installing) {
296  entry.setStatus(KNS3::Entry::Downloadable);
297  } else if (entry.status() == KNS3::Entry::Updating) {
298  entry.setStatus(KNS3::Entry::Updateable);
299  }
300  Q_EMIT signalEntryChanged(entry);
301  Q_EMIT signalInstallationFailed(i18n("Could not install \"%1\": file not found.", entry.name()));
302  return;
303  }
304 
305  entry.setInstalledFiles(installedFiles);
306 
307  auto installationFinished = [this, entry]() {
308  EntryInternal newentry = entry;
309  if (!newentry.updateVersion().isEmpty()) {
310  newentry.setVersion(newentry.updateVersion());
311  }
312  if (newentry.updateReleaseDate().isValid()) {
313  newentry.setReleaseDate(newentry.updateReleaseDate());
314  }
315  newentry.setStatus(KNS3::Entry::Installed);
316  Q_EMIT signalEntryChanged(newentry);
317  Q_EMIT signalInstallationFinished();
318  };
319  if (!postInstallationCommand.isEmpty()) {
320  QString scriptArgPath = !installedFiles.isEmpty() ? installedFiles.first() : targetPath;
321  if (scriptArgPath.endsWith(QLatin1Char('*'))) {
322  scriptArgPath = scriptArgPath.left(scriptArgPath.lastIndexOf(QLatin1Char('*')));
323  }
324  QProcess* p = runPostInstallationCommand(scriptArgPath);
325  connect(p, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
326  [entry, installationFinished, this] (int exitCode, QProcess::ExitStatus) {
327  if (exitCode) {
328  EntryInternal newEntry = entry;
329  newEntry.setStatus(KNS3::Entry::Invalid);
330  Q_EMIT signalEntryChanged(newEntry);
331  Q_EMIT signalInstallationFailed(i18n("Failed to execute install script"));
332  } else {
333  installationFinished();
334  }
335  });
336  } else {
337  installationFinished();
338  }
339  }
340 }
341 
343 {
344  // installdir is the target directory
345  QString installdir;
346 
347 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 79)
348  const bool userScope = scope == ScopeUser;
349 #else
350  const bool userScope = true;
351 #endif
352  // installpath also contains the file name if it's a single file, otherwise equal to installdir
353  int pathcounter = 0;
354  //wallpaper is already managed in the case of !xdgTargetDirectory.isEmpty()
355  if (!standardResourceDirectory.isEmpty() && standardResourceDirectory != QLatin1String("wallpaper")) {
357  //crude translation KStandardDirs names -> QStandardPaths enum
358  if (standardResourceDirectory == QLatin1String("tmp")) {
359  location = QStandardPaths::TempLocation;
360  } else if (standardResourceDirectory == QLatin1String("config")) {
362  }
363 
364  if (userScope) {
365  installdir = QStandardPaths::writableLocation(location);
366  } else { // system scope
367  installdir = QStandardPaths::standardLocations(location).constLast();
368  }
369  pathcounter++;
370  }
371  if (!targetDirectory.isEmpty() && targetDirectory != QLatin1String("/")) {
372  if (userScope) {
374  } else { // system scope
376  }
377  pathcounter++;
378  }
379  if (!xdgTargetDirectory.isEmpty() && xdgTargetDirectory != QLatin1String("/")) {
380  installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + xdgTargetDirectory + QLatin1Char('/');
381  pathcounter++;
382  }
383  if (!installPath.isEmpty()) {
384 #if defined(Q_OS_WIN)
385  WCHAR wPath[MAX_PATH + 1];
386  if (SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, wPath) == S_OK) {
387  installdir = QString::fromUtf16((const ushort *) wPath) + QLatin1Char('/') + installPath + QLatin1Char('/');
388  } else {
389  installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/');
390  }
391 #else
392  installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/');
393 #endif
394  pathcounter++;
395  }
396  if (!absoluteInstallPath.isEmpty()) {
397  installdir = absoluteInstallPath + QLatin1Char('/');
398  pathcounter++;
399  }
400 
401  if (pathcounter != 1) {
402  qCCritical(KNEWSTUFFCORE) << "Wrong number of installation directories given.";
403  return QString();
404  }
405 
406  qCDebug(KNEWSTUFFCORE) << "installdir: " << installdir;
407 
408  // create the dir if it doesn't exist (QStandardPaths doesn't create it, unlike KStandardDirs!)
409  QDir().mkpath(installdir);
410 
411  return installdir;
412 }
413 
414 QStringList Installation::installDownloadedFileAndUncompress(const KNSCore::EntryInternal &entry, const QString &payloadfile, const QString installdir)
415 {
416  // Collect all files that were installed
417  QStringList installedFiles;
418  bool isarchive = true;
419  UncompressionOptions uncompressionOpt = uncompressionSetting();
420 
421  // respect the uncompress flag in the knsrc
422  if (uncompressionOpt == UseKPackageUncompression) {
423  qCDebug(KNEWSTUFFCORE) << "Using KPackage for installation";
424  KPackage::PackageStructure structure;
425  KPackage::Package package(&structure);
426  QString serviceType;
427  package.setPath(payloadfile);
428  auto resetEntryStatus = [this,entry](){
429  KNSCore::EntryInternal changedEntry(entry);
430  if (changedEntry.status() == KNS3::Entry::Installing || changedEntry.status() == KNS3::Entry::Installed) {
431  changedEntry.setStatus(KNS3::Entry::Downloadable);
432  } else if (changedEntry.status() == KNS3::Entry::Updating) {
433  changedEntry.setStatus(KNS3::Entry::Updateable);
434  }
435  Q_EMIT signalEntryChanged(changedEntry);
436  };
437  if (package.isValid() && package.metadata().isValid()) {
438  qCDebug(KNEWSTUFFCORE) << "Package metadata is valid";
439  serviceType = package.metadata().value(QStringLiteral("X-Plasma-ServiceType"));
440  if (serviceType.isEmpty() && !package.metadata().serviceTypes().isEmpty()) {
441  serviceType = package.metadata().serviceTypes().first();
442  }
443  if (serviceType.isEmpty()) {
444  serviceType = property("kpackageType").toString();
445  }
446  if (!serviceType.isEmpty()) {
447  qCDebug(KNEWSTUFFCORE) << "Service type discovered as" << serviceType;
449  if (structure) {
450  KPackage::Package installer = KPackage::Package(structure);
451  if (installer.hasValidStructure()) {
453  qCDebug(KNEWSTUFFCORE) << "About to attempt to install" << package.metadata().pluginId() << "into" << packageRoot;
454  const QString expectedDir{packageRoot + package.metadata().pluginId()};
455  KJob *installJob = KPackageJob::update(payloadfile, packageRoot, serviceType);
456  // TODO KF6 Really, i would prefer to make more functions to handle this, but as this is
457  // an exported class, i'd rather not pollute the public namespace with internal functions,
458  // and we don't have a pimpl, so... we'll just have to deal with it for now
459  connect(installJob, &KJob::result, this, [this,entry,payloadfile,expectedDir,resetEntryStatus](KJob* job){
460  if (job->error() == KJob::NoError) {
461  if (QFile::exists(expectedDir)) {
462  EntryInternal newentry = entry;
463  newentry.setInstalledFiles(QStringList{expectedDir});
464  // update version and release date to the new ones
465  if (newentry.status() == KNS3::Entry::Updating) {
466  if (!newentry.updateVersion().isEmpty()) {
467  newentry.setVersion(newentry.updateVersion());
468  }
469  if (newentry.updateReleaseDate().isValid()) {
470  newentry.setReleaseDate(newentry.updateReleaseDate());
471  }
472  }
473  newentry.setStatus(KNS3::Entry::Installed);
474  // We can remove the downloaded file, because we don't save its location and don't need it to uninstall the entry
475  QFile::remove(payloadfile);
476  Q_EMIT signalEntryChanged(newentry);
477  Q_EMIT signalInstallationFinished();
478  qCDebug(KNEWSTUFFCORE) << "Install job finished with no error and we now have files" << expectedDir;
479  } else {
480  Q_EMIT signalInstallationFailed(i18n("The installation of %1 failed to create the expected new directory %2").arg(payloadfile, expectedDir));
481  resetEntryStatus();
482  qCDebug(KNEWSTUFFCORE) << "Install job finished with no error, but we do not have the expected new directory" << expectedDir;
483  }
484  } else {
485  if (job->error() == KPackage::Package::JobError::NewerVersionAlreadyInstalledError) {
486  EntryInternal newentry = entry;
487  newentry.setStatus(KNS3::Entry::Installed);
488  newentry.setInstalledFiles(QStringList{expectedDir});
489  // update version and release date to the new ones
490  if (!newentry.updateVersion().isEmpty()) {
491  newentry.setVersion(newentry.updateVersion());
492  }
493  if (newentry.updateReleaseDate().isValid()) {
494  newentry.setReleaseDate(newentry.updateReleaseDate());
495  }
496  Q_EMIT signalEntryChanged(newentry);
497  Q_EMIT signalInstallationFinished();
498  qCDebug(KNEWSTUFFCORE) << "Install job finished telling us this item was already installed with this version, so... let's just make a small fib and say we totally installed that, honest, and we now have files" << expectedDir;
499  } else {
500  Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2", payloadfile, job->errorText()));
501  resetEntryStatus();
502  qCDebug(KNEWSTUFFCORE) << "Install job finished with error state" << job->error() << "and description" << job->error();
503  }
504  }
505  });
506  installJob->start();
507  } else {
508  Q_EMIT signalInstallationFailed(i18n("The installation of %1 failed, as the service type %2 was not accepted by the system (did you forget to install the KPackage support plugin for this type of package?)", payloadfile, serviceType));
509  resetEntryStatus();
510  qCWarning(KNEWSTUFFCORE) << "Package serviceType" << serviceType << "not found";
511  }
512  } else {
513  // no package structure
514  Q_EMIT signalInstallationFailed(i18n("The installation of %1 failed, as the downloaded package does not contain a correct KPackage structure.", payloadfile));
515  resetEntryStatus();
516  qCWarning(KNEWSTUFFCORE) << "Could not load the package structure for KPackage service type" << serviceType;
517  }
518  } else {
519  // no service type
520  Q_EMIT signalInstallationFailed(i18n("The installation of %1 failed, as the downloaded package does not list a service type.", payloadfile));
521  resetEntryStatus();
522  qCWarning(KNEWSTUFFCORE) << "No service type listed in" << payloadfile;
523  }
524  } else {
525  // package or package metadata is invalid
526  Q_EMIT signalInstallationFailed(i18n("The installation of %1 failed, as the downloaded package does not contain any useful meta information, which means it is not a valid KPackage.", payloadfile));
527  resetEntryStatus();
528  qCWarning(KNEWSTUFFCORE) << "No valid meta information (which suggests no valid KPackage) found in" << payloadfile;
529  }
530  } else {
531  if (uncompressionOpt == AlwaysUncompress
532  || uncompressionOpt == UncompressIntoSubdirIfArchive
533  || uncompressionOpt == UncompressIfArchive
534  || uncompressionOpt == UncompressIntoSubdir) {
535  // this is weird but a decompression is not a single name, so take the path instead
536  QMimeDatabase db;
537  QMimeType mimeType = db.mimeTypeForFile(payloadfile);
538  qCDebug(KNEWSTUFFCORE) << "Postinstallation: uncompress the file";
539 
540  // FIXME: check for overwriting, malicious archive entries (../foo) etc.
541  // FIXME: KArchive should provide "safe mode" for this!
542  QScopedPointer<KArchive> archive;
543 
544  if (mimeType.inherits(QStringLiteral("application/zip"))) {
545  archive.reset(new KZip(payloadfile));
546  } else if (mimeType.inherits(QStringLiteral("application/tar"))
547  || mimeType.inherits(QStringLiteral("application/x-gzip"))
548  || mimeType.inherits(QStringLiteral("application/x-bzip"))
549  || mimeType.inherits(QStringLiteral("application/x-lzma"))
550  || mimeType.inherits(QStringLiteral("application/x-xz"))
551  || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar"))
552  || mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) {
553  archive.reset(new KTar(payloadfile));
554  } else {
555  qCCritical(KNEWSTUFFCORE) << "Could not determine type of archive file '" << payloadfile << "'";
556  if (uncompressionOpt == AlwaysUncompress) {
557  Q_EMIT signalInstallationError(i18n("Could not determine the type of archive of the downloaded file %1", payloadfile));
558  return QStringList();
559  }
560  isarchive = false;
561  }
562 
563  if (isarchive) {
564  bool success = archive->open(QIODevice::ReadOnly);
565  if (!success) {
566  qCCritical(KNEWSTUFFCORE) << "Cannot open archive file '" << payloadfile << "'";
567  if (uncompressionOpt == AlwaysUncompress) {
568  Q_EMIT signalInstallationError(i18n("Failed to open the archive file %1. The reported error was: %2", payloadfile, archive->errorString()));
569  return QStringList();
570  }
571  // otherwise, just copy the file
572  isarchive = false;
573  }
574 
575  if (isarchive) {
576  const KArchiveDirectory *dir = archive->directory();
577  //if there is more than an item in the file, and we are requested to do so
578  //put contents in a subdirectory with the same name as the file
579  QString installpath;
580  const bool isSubdir = (uncompressionOpt == UncompressIntoSubdir || uncompressionOpt == UncompressIntoSubdirIfArchive) && dir->entries().count() > 1;
581  if (isSubdir) {
582  installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName();
583  } else {
584  installpath = installdir;
585  }
586 
587  if (dir->copyTo(installpath)) {
588  // If we extracted the subdir we want to save it using the /* notation like we would when using the "archive" option
589  // Also if we use an (un)install command we only call it once with the folder as argument and not for each file
590  if (isSubdir) {
591  installedFiles << QDir(installpath).absolutePath() + QLatin1String("/*");
592  } else {
593  installedFiles << archiveEntries(installpath, dir);
594  }
595  } else
596  qCWarning(KNEWSTUFFCORE) << "could not install" << entry.name() << "to" << installpath;
597 
598  archive->close();
599  QFile::remove(payloadfile);
600  }
601  }
602  }
603 
604  qCDebug(KNEWSTUFFCORE) << "isarchive: " << isarchive;
605 
606  //some wallpapers are compressed, some aren't
607  if ((!isarchive && standardResourceDirectory == QLatin1String("wallpaper")) ||
608  (uncompressionOpt == NeverUncompress
609  || (uncompressionOpt == UncompressIfArchive && !isarchive)
610  || (uncompressionOpt == UncompressIntoSubdirIfArchive && !isarchive))
611  ) {
612  // no decompress but move to target
613 
615  // FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names
616  QUrl source = QUrl(entry.payload());
617  qCDebug(KNEWSTUFFCORE) << "installing non-archive from " << source.url();
618 #if KNEWSTUFF_BUILD_DEPRECATED_SINCE(5, 79)
619  QString installfile;
620  QString ext = source.fileName().section(QLatin1Char('.'), -1);
621  if (customName) {
622  // Otherwise name can be interpreted as path
623  installfile = entry.name().remove(QLatin1Char('/'));
624  if (!entry.version().isEmpty()) {
625  installfile += QLatin1Char('-') + entry.version();
626  }
627  if (!ext.isEmpty()) {
628  installfile += QLatin1Char('.') + ext;
629  }
630  } else {
631  installfile = source.fileName();
632  }
633  const QString installpath = QDir(installdir).filePath(installfile);
634 #else
635  const QString installpath = QDir(installdir).filePath(source.fileName());
636 #endif
637 
638  qCDebug(KNEWSTUFFCORE) << "Install to file " << installpath;
639  // FIXME: copy goes here (including overwrite checking)
640  // FIXME: what must be done now is to update the cache *again*
641  // in order to set the new payload filename (on root tag only)
642  // - this might or might not need to take uncompression into account
643  // FIXME: for updates, we might need to force an overwrite (that is, deleting before)
644  QFile file(payloadfile);
645  bool success = true;
646  const bool update = ((entry.status() == KNS3::Entry::Updateable) || (entry.status() == KNS3::Entry::Updating));
647 
648  if (QFile::exists(installpath) && QDir::tempPath() != installdir) {
649  if (!update) {
650  Question question(Question::YesNoQuestion);
651  question.setQuestion(i18n("This file already exists on disk (possibly due to an earlier failed download attempt). Continuing means overwriting it. Do you wish to overwrite the existing file?") + QStringLiteral("\n'") + installpath + QLatin1Char('\''));
652  question.setTitle(i18n("Overwrite File"));
653  if(question.ask() != Question::YesResponse) {
654  return QStringList();
655  }
656  }
657  success = QFile::remove(installpath);
658  }
659  if (success) {
660  //remove in case it's already present and in a temporary directory, so we get to actually use the path again
661  if (installpath.startsWith(QDir::tempPath())) {
662  file.remove(installpath);
663  }
664  success = file.rename(installpath);
665  qCDebug(KNEWSTUFFCORE) << "move: " << file.fileName() << " to " << installpath;
666  }
667  if (!success) {
668  Q_EMIT signalInstallationError(i18n("Unable to move the file %1 to the intended destination %2", payloadfile, installpath));
669  qCCritical(KNEWSTUFFCORE) << "Cannot move file '" << payloadfile << "' to destination '" << installpath << "'";
670  return QStringList();
671  }
672  installedFiles << installpath;
673  }
674  }
675 
676  return installedFiles;
677 }
678 
679 QProcess* Installation::runPostInstallationCommand(const QString &installPath)
680 {
681  QString command(postInstallationCommand);
682  QString fileArg(KShell::quoteArg(installPath));
683  command.replace(QLatin1String("%f"), fileArg);
684 
685  qCDebug(KNEWSTUFFCORE) << "Run command: " << command;
686 
687  QProcess* ret = new QProcess(this);
688  connect(ret, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, [this, command, ret](int exitcode, QProcess::ExitStatus status) {
690  if (status == QProcess::CrashExit) {
691  Q_EMIT signalInstallationError(i18n("The installation failed while attempting to run the command:\n%1\n\nThe returned output was:\n%2", command, output));
692  qCCritical(KNEWSTUFFCORE) << "Process crashed with command: " << command;
693  } else if (exitcode) {
694  Q_EMIT signalInstallationError(i18n("The installation failed with code %1 while attempting to run the command:\n%2\n\nThe returned output was:\n%3", exitcode, command, output));
695  qCCritical(KNEWSTUFFCORE) << "Command '" << command << "' failed with code" << exitcode;
696  }
697  sender()->deleteLater();
698  });
699 
700  QStringList args = KShell::splitArgs(command);
701  ret->setProgram(args.takeFirst());
702  ret->setArguments(args);
703  ret->start();
704  return ret;
705 }
706 
708 {
709  // TODO Put this in pimpl or job
710  const auto deleteFilesAndMarkAsUninstalled = [entry, this](){
711  const auto lst = entry.installedFiles();
712  for (const QString &file : lst) {
713  // This was used to delete the download location if there are no more entries
714  if (file.endsWith(QLatin1Char('/'))) {
715  QDir().rmdir(file);
716  } else if (file.endsWith(QLatin1String("/*"))) {
717  QDir dir(file.left(file.size() - 2));
718  bool worked = dir.removeRecursively();
719  if (!worked) {
720  qCWarning(KNEWSTUFFCORE) << "Couldn't remove" << dir.path();
721  continue;
722  }
723  } else {
724  QFileInfo info(file);
725  if (info.exists() || info.isSymLink()) {
726  bool worked = QFile::remove(file);
727  if (!worked) {
728  qWarning() << "unable to delete file " << file;
729  return;
730  }
731  } else {
732  qWarning() << "unable to delete file " << file << ". file does not exist.";
733  }
734  }
735  }
736  EntryInternal newEntry = entry;
737  newEntry.setUnInstalledFiles(entry.installedFiles());
738  newEntry.setInstalledFiles(QStringList());
739  newEntry.setStatus(KNS3::Entry::Deleted);
740  Q_EMIT signalEntryChanged(newEntry);
741  };
742 
744  const auto lst = entry.installedFiles();
745  if (lst.length() == 1) {
746  const QString installedFile{lst.first()};
747  if (QFileInfo(installedFile).isDir()) {
748  KPackage::PackageStructure structure;
749  KPackage::Package package(&structure);
750  package.setPath(installedFile);
751  if (package.isValid() && package.metadata().isValid()) {
752  QString serviceType = package.metadata().value(QStringLiteral("X-Plasma-ServiceType"));
753  if (serviceType.isEmpty() && !package.metadata().serviceTypes().isEmpty()) {
754  serviceType = package.metadata().serviceTypes().first();
755  }
756  if (serviceType.isEmpty()) {
757  serviceType = property("kpackageType").toString();
758  }
759  if (!serviceType.isEmpty()) {
761  if (structure) {
762  KPackage::Package installer = KPackage::Package(structure);
763  if (!installer.hasValidStructure()) {
764  qWarning() << "Package serviceType" << serviceType << "not found";
765  }
767  KJob *removalJob = KPackageJob::uninstall(package.metadata().pluginId(), packageRoot, serviceType);
768  connect(removalJob, &KJob::result, this, [this,installedFile,installer,entry](KJob* job){
769  EntryInternal newEntry = entry;
770  if (job->error() == KJob::NoError) {
771  newEntry.setStatus(KNS3::Entry::Deleted);
772  newEntry.setUnInstalledFiles(newEntry.installedFiles());
773  newEntry.setInstalledFiles(QStringList());
774  Q_EMIT signalEntryChanged(newEntry);
775  } else {
776  Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2", installedFile, job->errorText()));
777  }
778  });
779  removalJob->start();
780  } else {
781  // no package structure
782  Q_EMIT signalInstallationFailed(i18n("The removal of %1 failed, as the installed package does not contain a correct KPackage structure.", installedFile));
783  }
784  } else {
785  // no service type
786  Q_EMIT signalInstallationFailed(i18n("The removal of %1 failed, as the installed package is not a supported type (did you forget to install the KPackage support plugin for this type of package?)", installedFile));
787  }
788  } else {
789  // package or package metadata is invalid
790  Q_EMIT signalInstallationFailed(i18n("The removal of %1 failed, as the installed package does not contain any useful meta information, which means it is not a valid KPackage.", entry.name()));
791  }
792  } else {
793  QMimeDatabase db;
794  QMimeType mimeType = db.mimeTypeForFile(installedFile);
795  if (mimeType.inherits(QStringLiteral("application/zip")) ||
796  mimeType.inherits(QStringLiteral("application/x-compressed-tar")) ||
797  mimeType.inherits(QStringLiteral("application/x-gzip")) ||
798  mimeType.inherits(QStringLiteral("application/x-tar")) ||
799  mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) ||
800  mimeType.inherits(QStringLiteral("application/x-xz")) ||
801  mimeType.inherits(QStringLiteral("application/x-lzma"))) {
802  // Then it's one of the downloaded files installed with an old version of knewstuff prior to
803  // the native kpackage support being added, and we need to do some inspection-and-removal work...
804  KPackage::PackageStructure structure;
805  KPackage::Package package(&structure);
806  const QString serviceType{property("kpackageType").toString()};
807  package.setPath(installedFile);
808  if (package.isValid() && package.metadata().isValid()) {
809  // try and load the kpackage and sniff the expected location of its installation, and ask KPackage to remove that thing, if it's there
811  if (structure) {
812  KPackage::Package installer = KPackage::Package(structure);
813  if (installer.hasValidStructure()) {
815  qCDebug(KNEWSTUFFCORE) << "About to attempt to uninstall" << package.metadata().pluginId() << "from" << packageRoot;
816  // Frankly, we don't care whether or not this next step succeeds, and it can just fizzle if it wants
817  // to. This is a cleanup step, and if it fails, it's just not really important.
818  KPackageJob::uninstall(package.metadata().pluginId(), packageRoot, serviceType);
819  }
820  }
821  }
822  // Also get rid of the downloaded file, and tell everything they've gone
823  if (QFile::remove(installedFile)) {
824  entry.setStatus(KNS3::Entry::Deleted);
825  entry.setUnInstalledFiles(entry.installedFiles());
827  Q_EMIT signalEntryChanged(entry);
828  } else {
829  Q_EMIT signalInstallationFailed(i18n("The removal of %1 failed, as the downloaded file %2 could not be automatically removed.", entry.name(), installedFile));
830  }
831  } else {
832  // Not sure what's installed, but it's not a KPackage, not a lot we can do with this...
833  Q_EMIT signalInstallationFailed(i18n("The removal of %1 failed, due to the installed file not being a KPackage. The file in question was %2, and you can attempt to delete it yourself, if you are certain that it is not needed.", entry.name(), installedFile));
834  }
835  }
836  } else {
837  Q_EMIT signalInstallationFailed(i18n("The removal of %1 failed, as there seems to somehow be more than one thing installed, which is not supposed to be possible for KPackage based entries.", entry.name()));
838  }
839  deleteFilesAndMarkAsUninstalled();
840  } else {
841  const auto lst = entry.installedFiles();
842  // If there is an uninstall script, make sure it runs without errors
843  if (!uninstallCommand.isEmpty()) {
844  bool validFileExisted = false;
845  for (const QString &file : lst) {
846  QString filePath = file;
847  bool validFile = QFileInfo::exists(filePath);
848  // If we have uncompressed a subdir we write <path>/* in the config, but when calling a script
849  // we want to convert this to a normal path
850  if (!validFile && file.endsWith(QLatin1Char('*'))) {
851  filePath = filePath.left(filePath.lastIndexOf(QLatin1Char('*')));
852  validFile = QFileInfo::exists(filePath);
853  }
854  if (validFile) {
855  validFileExisted = true;
856  QString fileArg(KShell::quoteArg(filePath));
857  QString command(uninstallCommand);
858  command.replace(QLatin1String("%f"), fileArg);
859 
860  QStringList args = KShell::splitArgs(command);
861  const QString program = args.takeFirst();
862  QProcess *process = new QProcess(this);
863  process->start(program, args);
864  connect(process, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
865  [this, command, process, entry, deleteFilesAndMarkAsUninstalled](int, QProcess::ExitStatus status) {
866  if (status == QProcess::CrashExit) {
867  const QString processOutput = QString::fromLocal8Bit(process->readAllStandardError());
868  const QString err = i18n("The uninstallation process failed to successfully run the command %1\n"
869  "The output of was: \n%2\n"
870  "If you think this is incorrect, you can continue or cancel the uninstallation process",
871  KShell::quoteArg(command), processOutput);
873  // Ask the user if he wants to continue, even though the script failed
874  Question question(Question::ContinueCancelQuestion);
875  question.setQuestion(err);
876  Question::Response response = question.ask();
877  if (response == Question::CancelResponse) {
878  // Use can delete files manually
879  EntryInternal newEntry = entry;
880  newEntry.setStatus(KNS3::Entry::Installed);
881  Q_EMIT signalEntryChanged(newEntry);
882  return;
883  }
884  } else {
885  qCDebug(KNEWSTUFFCORE) << "Command executed successfully: " << command;
886  }
887  deleteFilesAndMarkAsUninstalled();
888  });
889  }
890  }
891  // If the entry got deleted, but the RemoveDeadEntries option was not selected this case can happen
892  if (!validFileExisted) {
893  deleteFilesAndMarkAsUninstalled();
894  }
895  } else {
896  deleteFilesAndMarkAsUninstalled();
897  }
898  }
899 }
900 
902 {
903  return property("uncompressSetting").value<UncompressionOptions>();
904 }
905 
906 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 31)
907 void Installation::slotInstallationVerification(int result)
908 {
909  Q_UNUSED(result)
910  // Deprecated, was wired up to defunct Security class.
911 }
912 #endif
913 
914 QStringList Installation::archiveEntries(const QString &path, const KArchiveDirectory *dir)
915 {
916  QStringList files;
917  const auto lst = dir->entries();
918  for (const QString &entry : lst) {
919  const auto currentEntry = dir->entry(entry);
920 
921  const QString childPath = QDir(path).filePath(entry);
922  if (currentEntry->isFile()) {
923  files << childPath;
924  } else if (currentEntry->isDirectory()) {
925  files << childPath + QStringLiteral("/*");
926  }
927  }
928  return files;
929 }
930 
QString url(QUrl::FormattingOptions options) const const
QStringList serviceTypes() const
static KPackageJob * uninstall(const QString &packageName, const QString &packageRoot, const QString &serviceType)
Create a job for removing the given installed package.
A way to ask a user a question from inside a GUI-less library (like KNewStuffCore) ...
Definition: question.h:41
QString value(const QString &key, const QString &defaultValue=QString()) const
QString writableLocation(QStandardPaths::StandardLocation type)
bool remove()
QObject * sender() const const
virtual QString errorString() const
< As Archive, except that if there is more than an item in the file, put contents in a subdirectory w...
Definition: installation.h:65
UncompressionOptions uncompressionSetting() const
Returns the uncompression setting, in a computer-readable format.
bool rename(const QString &newName)
virtual QString fileName() const const override
Contains the core functionality for handling interaction with NewStuff providers. ...
void setUnInstalledFiles(const QStringList &files)
Set the files that have been uninstalled by the uninstall command.
bool removeRecursively()
QString filePath(const QString &fileName) const const
bool inherits(const QString &mimeTypeName) const const
T value() const const
static KPackageJob * update(const QString &sourcePackage, const QString &packageRoot, const QString &serviceType)
Create a job for updating the given package, or installing it if it is not already, the given package into the package root, and treat it as the given service type.
QString defaultPackageRoot() const
bool exists() const const
QString & remove(int position, int n)
void setArguments(const QStringList &arguments)
QString homePath()
bool hasValidStructure() const
< Never attempt to decompress a file, whatever format it is. Matches "never" knsrc setting ...
Definition: installation.h:61
QStringList standardLocations(QStandardPaths::StandardLocation type)
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
void reset(T *other)
bool copyTo(const QString &dest, bool recursive=true) const
QMimeType mimeTypeForFile(const QString &fileName, QMimeDatabase::MatchMode mode) const const
static PackageLoader * self()
void setInstalledFiles(const QStringList &files)
Set the files that have been installed by the install command.
QString payload() const
Retrieve the file name of the object.
const T & constLast() const const
int count(const T &value) const const
QString fromLocal8Bit(const char *str, int size)
QString version() const
Retrieve the version string of the object.
bool rmdir(const QString &dirName) const const
QVariant property(const char *name) const const
QString path() const const
QString tempPath()
QString fromUtf16(const ushort *unicode, int size)
void setPath(const QString &path)
bool isEmpty() const const
QStringList installedFiles() const
Retrieve the locally installed files.
bool isEmpty() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
void finished(int exitCode)
bool isValid() const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
void deleteLater()
T & first()
QDate updateReleaseDate() const
Retrieve the date of the newer version that is available as update.
void setStatus(KNS3::Entry::Status status)
Sets the entry&#39;s status.
KCOREADDONS_EXPORT QStringList splitArgs(const QString &cmd, Options flags=NoOptions, Errors *err=nullptr)
Installation(QObject *parent=nullptr)
Constructor.
KPackage::PackageStructure * loadPackageStructure(const QString &packageFormat)
void setProgram(const QString &program)
bool exists() const const
KCOREADDONS_EXPORT QString quoteArg(const QString &arg)
void signalInstallationError(const QString &message)
An informational signal fired when a serious error occurs during the installation.
< If the file is an archive, decompress it in a subdirectory if it contains multiple files...
Definition: installation.h:64
const KArchiveEntry * entry(const QString &name) const
QString targetInstallationPath() const
void uninstall(KNSCore::EntryInternal entry)
Uninstalls an entry.
void setVersion(const QString &version)
Sets the version number.
QString i18n(const char *text, const TYPE &arg...)
QString & replace(int position, int n, QChar after)
< Assume all downloaded files are archives, and attempt to decompress them. Will cause failure if dec...
Definition: installation.h:62
bool isValid() const const
QString mid(int position, int n) const const
KPluginMetaData metadata() const
T takeFirst()
KNewStuff data entry container.
Definition: entryinternal.h:49
QString absolutePath() const const
virtual Q_SCRIPTABLE void start()=0
int length() const const
bool isValid() const
QString section(QChar sep, int start, int end, QString::SectionFlags flags) const const
QString left(int n) const const
QString updateVersion() const
Retrieve the version string of the object that is available as update.
bool setProperty(const char *name, const QVariant &value)
void result(KJob *job)
KNS3::Entry::Status status() const
Retrieves the entry&#39;s status.
void install(const KNSCore::EntryInternal &entry)
Installs an entry&#39;s payload file.
bool openUrl(const QUrl &url)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QString name() const
Retrieve the name of the data object.
QString toString() const const
T readEntry(const QString &key, const T &aDefault) const
void downloadPayload(const KNSCore::EntryInternal &entry)
Downloads a payload file.
QString fileName(QUrl::ComponentFormattingOptions options) const const
QStringList entries() const
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
bool isValid() const
Q_EMITQ_EMIT
QString pluginId() const
void setReleaseDate(const QDate &releasedate)
Sets the release date.
QByteArray readAllStandardError()
bool mkpath(const QString &dirPath) const const
QString errorText() const
QUrl fromLocalFile(const QString &localFile)
< If the file is an archive, decompress it, otherwise just pass it on. Matches "archive" knsrc settin...
Definition: installation.h:63
QString locate(QStandardPaths::StandardLocation type, const QString &fileName, QStandardPaths::LocateOptions options)
int error() const
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Mon Jan 18 2021 22:43:50 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.