KNewStuff

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

KDE's Doxygen guidelines are available online.