KNewStuff
installation.cpp
Go to the documentation of this file.
00001 /* 00002 This file is part of KNewStuff2. 00003 Copyright (c) 2007 Josef Spillner <spillner@kde.org> 00004 Copyright (C) 2009 Frederik Gladhorn <gladhorn@kde.org> 00005 00006 This library is free software; you can redistribute it and/or 00007 modify it under the terms of the GNU Lesser General Public 00008 License as published by the Free Software Foundation; either 00009 version 2.1 of the License, or (at your option) any later version. 00010 00011 This library is distributed in the hope that it will be useful, 00012 but WITHOUT ANY WARRANTY; without even the implied warranty of 00013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00014 Lesser General Public License for more details. 00015 00016 You should have received a copy of the GNU Lesser General Public 00017 License along with this library. If not, see <http://www.gnu.org/licenses/>. 00018 */ 00019 00020 #include "installation.h" 00021 00022 #include <QDir> 00023 #include <QFile> 00024 00025 #include "kstandarddirs.h" 00026 #include "kmimetype.h" 00027 #include "karchive.h" 00028 #include "kzip.h" 00029 #include "ktar.h" 00030 #include "kprocess.h" 00031 #include "kio/job.h" 00032 #include "krandom.h" 00033 #include "kshell.h" 00034 #include "kmessagebox.h" // TODO get rid of message box 00035 #include "ktoolinvocation.h" // TODO remove, this was only for my playing round 00036 #include "klocalizedstring.h" 00037 #include "kdebug.h" 00038 00039 #include "core/security.h" 00040 #ifdef Q_OS_WIN 00041 #include <windows.h> 00042 #include <shlobj.h> 00043 #endif 00044 00045 using namespace KNS3; 00046 00047 Installation::Installation(QObject* parent) 00048 : QObject(parent) 00049 , checksumPolicy(Installation::CheckIfPossible) 00050 , signaturePolicy(Installation::CheckIfPossible) 00051 , scope(Installation::ScopeUser) 00052 , customName(false) 00053 , acceptHtml(false) 00054 { 00055 } 00056 00057 bool Installation::readConfig(const KConfigGroup& group) 00058 { 00059 // FIXME: add support for several categories later on 00060 // FIXME: read out only when actually installing as a performance improvement? 00061 QString uncompresssetting = group.readEntry("Uncompress", QString("never")); 00062 // support old value of true as equivalent of always 00063 if (uncompresssetting == "true") { 00064 uncompresssetting = "always"; 00065 } 00066 if (uncompresssetting != "always" && uncompresssetting != "archive" && uncompresssetting != "never") { 00067 kError() << "invalid Uncompress setting chosen, must be one of: always, archive, or never" << endl; 00068 return false; 00069 } 00070 uncompression = uncompresssetting; 00071 postInstallationCommand = group.readEntry("InstallationCommand", QString()); 00072 uninstallCommand = group.readEntry("UninstallCommand", QString()); 00073 standardResourceDirectory = group.readEntry("StandardResource", QString()); 00074 targetDirectory = group.readEntry("TargetDir", QString()); 00075 xdgTargetDirectory = group.readEntry("XdgTargetDir", QString()); 00076 installPath = group.readEntry("InstallPath", QString()); 00077 absoluteInstallPath = group.readEntry("AbsoluteInstallPath", QString()); 00078 customName = group.readEntry("CustomName", false); 00079 acceptHtml = group.readEntry("AcceptHtmlDownloads", false); 00080 00081 if (standardResourceDirectory.isEmpty() && 00082 targetDirectory.isEmpty() && 00083 xdgTargetDirectory.isEmpty() && 00084 installPath.isEmpty() && 00085 absoluteInstallPath.isEmpty()) { 00086 kError() << "No installation target set"; 00087 return false; 00088 } 00089 00090 QString checksumpolicy = group.readEntry("ChecksumPolicy", QString()); 00091 if (!checksumpolicy.isEmpty()) { 00092 if (checksumpolicy == "never") 00093 checksumPolicy = Installation::CheckNever; 00094 else if (checksumpolicy == "ifpossible") 00095 checksumPolicy = Installation::CheckIfPossible; 00096 else if (checksumpolicy == "always") 00097 checksumPolicy = Installation::CheckAlways; 00098 else { 00099 kError() << "The checksum policy '" + checksumpolicy + "' is unknown." << endl; 00100 return false; 00101 } 00102 } 00103 00104 QString signaturepolicy = group.readEntry("SignaturePolicy", QString()); 00105 if (!signaturepolicy.isEmpty()) { 00106 if (signaturepolicy == "never") 00107 signaturePolicy = Installation::CheckNever; 00108 else if (signaturepolicy == "ifpossible") 00109 signaturePolicy = Installation::CheckIfPossible; 00110 else if (signaturepolicy == "always") 00111 signaturePolicy = Installation::CheckAlways; 00112 else { 00113 kError() << "The signature policy '" + signaturepolicy + "' is unknown." << endl; 00114 return false; 00115 } 00116 } 00117 00118 QString scopeString = group.readEntry("Scope", QString()); 00119 if (!scopeString.isEmpty()) { 00120 if (scopeString == "user") 00121 scope = ScopeUser; 00122 else if (scopeString == "system") 00123 scope = ScopeSystem; 00124 else { 00125 kError() << "The scope '" + scopeString + "' is unknown." << endl; 00126 return false; 00127 } 00128 00129 if (scope == ScopeSystem) { 00130 if (!installPath.isEmpty()) { 00131 kError() << "System installation cannot be mixed with InstallPath." << endl; 00132 return false; 00133 } 00134 } 00135 } 00136 return true; 00137 } 00138 00139 bool Installation::isRemote() const 00140 { 00141 if (!installPath.isEmpty()) return false; 00142 if (!targetDirectory.isEmpty()) return false; 00143 if (!xdgTargetDirectory.isEmpty()) return false; 00144 if (!absoluteInstallPath.isEmpty()) return false; 00145 if (!standardResourceDirectory.isEmpty()) return false; 00146 return true; 00147 } 00148 00149 void Installation::install(EntryInternal entry) 00150 { 00151 downloadPayload(entry); 00152 } 00153 00154 void Installation::downloadPayload(const KNS3::EntryInternal& entry) 00155 { 00156 if(!entry.isValid()) { 00157 emit signalInstallationFailed(i18n("Invalid item.")); 00158 return; 00159 } 00160 KUrl source = KUrl(entry.payload()); 00161 00162 if (!source.isValid()) { 00163 kError() << "The entry doesn't have a payload." << endl; 00164 emit signalInstallationFailed(i18n("Download of item failed: no download URL for \"%1\".", entry.name())); 00165 return; 00166 } 00167 00168 // FIXME no clue what this is supposed to do 00169 if (isRemote()) { 00170 // Remote resource 00171 //kDebug() << "Relaying remote payload '" << source << "'"; 00172 install(entry, source.pathOrUrl()); 00173 emit signalPayloadLoaded(source); 00174 // FIXME: we still need registration for eventual deletion 00175 return; 00176 } 00177 00178 QString fileName(source.fileName()); 00179 KUrl destination = QString(KGlobal::dirs()->saveLocation("tmp") + KRandom::randomString(10) + '-' + fileName); 00180 kDebug() << "Downloading payload '" << source << "' to '" << destination << "'"; 00181 00182 // FIXME: check for validity 00183 KIO::FileCopyJob *job = KIO::file_copy(source, destination, -1, KIO::Overwrite | KIO::HideProgressInfo); 00184 connect(job, 00185 SIGNAL(result(KJob*)), 00186 SLOT(slotPayloadResult(KJob*))); 00187 00188 entry_jobs[job] = entry; 00189 } 00190 00191 00192 void Installation::slotPayloadResult(KJob *job) 00193 { 00194 // for some reason this slot is getting called 3 times on one job error 00195 if (entry_jobs.contains(job)) { 00196 EntryInternal entry = entry_jobs[job]; 00197 entry_jobs.remove(job); 00198 00199 if (job->error()) { 00200 emit signalInstallationFailed(i18n("Download of \"%1\" failed, error: %2", entry.name(), job->errorString())); 00201 } else { 00202 KIO::FileCopyJob *fcjob = static_cast<KIO::FileCopyJob*>(job); 00203 00204 // check if the app likes html files - disabled by default as too many bad links have been submitted to opendesktop.org 00205 if (!acceptHtml) { 00206 KMimeType::Ptr mimeType = KMimeType::findByPath(fcjob->destUrl().toLocalFile()); 00207 if (mimeType->is("text/html") || mimeType->is("application/x-php")) { 00208 if (KMessageBox::questionYesNo(0, 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?"), i18n("Possibly bad download link")) 00209 == KMessageBox::Yes) { 00210 KToolInvocation::invokeBrowser(fcjob->srcUrl().url()); 00211 emit signalInstallationFailed(i18n("Downloaded file was a HTML file. Opened in browser.")); 00212 entry.setStatus(Entry::Invalid); 00213 emit signalEntryChanged(entry); 00214 return; 00215 } 00216 } 00217 } 00218 00219 install(entry, fcjob->destUrl().toLocalFile()); 00220 emit signalPayloadLoaded(fcjob->destUrl()); 00221 } 00222 } 00223 } 00224 00225 00226 void Installation::install(KNS3::EntryInternal entry, const QString& downloadedFile) 00227 { 00228 kDebug() << "Install: " << entry.name() << " from " << downloadedFile; 00229 00230 if (entry.payload().isEmpty()) { 00231 kDebug() << "No payload associated with: " << entry.name(); 00232 return; 00233 } 00234 00235 // FIXME: first of all, do the security stuff here 00236 // this means check sum comparison and signature verification 00237 // signature verification might take a long time - make async?! 00238 /* 00239 if (checksumPolicy() != Installation::CheckNever) { 00240 if (entry.checksum().isEmpty()) { 00241 if (checksumPolicy() == Installation::CheckIfPossible) { 00242 //kDebug() << "Skip checksum verification"; 00243 } else { 00244 kError() << "Checksum verification not possible" << endl; 00245 return false; 00246 } 00247 } else { 00248 //kDebug() << "Verify checksum..."; 00249 } 00250 } 00251 if (signaturePolicy() != Installation::CheckNever) { 00252 if (entry.signature().isEmpty()) { 00253 if (signaturePolicy() == Installation::CheckIfPossible) { 00254 //kDebug() << "Skip signature verification"; 00255 } else { 00256 kError() << "Signature verification not possible" << endl; 00257 return false; 00258 } 00259 } else { 00260 //kDebug() << "Verify signature..."; 00261 } 00262 } 00263 */ 00264 00265 QString targetPath = targetInstallationPath(downloadedFile); 00266 QStringList installedFiles = installDownloadedFileAndUncompress(entry, downloadedFile, targetPath); 00267 00268 if (installedFiles.isEmpty()) { 00269 if (entry.status() == Entry::Installing) { 00270 entry.setStatus(Entry::Downloadable); 00271 } else if (entry.status() == Entry::Updating) { 00272 entry.setStatus(Entry::Updateable); 00273 } 00274 emit signalEntryChanged(entry); 00275 emit signalInstallationFailed(i18n("Could not install \"%1\": file not found.", entry.name())); 00276 return; 00277 } 00278 00279 entry.setInstalledFiles(installedFiles); 00280 00281 if (!postInstallationCommand.isEmpty()) { 00282 QString target; 00283 if (installedFiles.size() == 1) { 00284 runPostInstallationCommand(installedFiles.first()); 00285 } else { 00286 runPostInstallationCommand(targetPath); 00287 } 00288 } 00289 00290 // ==== FIXME: security code below must go above, when async handling is complete ==== 00291 00292 // FIXME: security object lifecycle - it is a singleton! 00293 Security *sec = Security::ref(); 00294 00295 connect(sec, 00296 SIGNAL(validityResult(int)), 00297 SLOT(slotInstallationVerification(int))); 00298 00299 // FIXME: change to accept filename + signature 00300 sec->checkValidity(QString()); 00301 00302 // update version and release date to the new ones 00303 if (entry.status() == Entry::Updating) { 00304 if (!entry.updateVersion().isEmpty()) { 00305 entry.setVersion(entry.updateVersion()); 00306 } 00307 if (entry.updateReleaseDate().isValid()) { 00308 entry.setReleaseDate(entry.updateReleaseDate()); 00309 } 00310 } 00311 00312 entry.setStatus(Entry::Installed); 00313 emit signalEntryChanged(entry); 00314 emit signalInstallationFinished(); 00315 } 00316 00317 QString Installation::targetInstallationPath(const QString& payloadfile) 00318 { 00319 QString installpath(payloadfile); 00320 QString installdir; 00321 00322 if (!isRemote()) { 00323 // installdir is the target directory 00324 00325 // installpath also contains the file name if it's a single file, otherwise equal to installdir 00326 int pathcounter = 0; 00327 if (!standardResourceDirectory.isEmpty()) { 00328 if (scope == ScopeUser) { 00329 installdir = KStandardDirs::locateLocal(standardResourceDirectory.toUtf8(), "/"); 00330 } else { // system scope 00331 installdir = KStandardDirs::installPath(standardResourceDirectory.toUtf8()); 00332 } 00333 pathcounter++; 00334 } 00335 if (!targetDirectory.isEmpty()) { 00336 if (scope == ScopeUser) { 00337 installdir = KStandardDirs::locateLocal("data", targetDirectory + '/'); 00338 } else { // system scope 00339 installdir = KStandardDirs::installPath("data") + targetDirectory + '/'; 00340 } 00341 pathcounter++; 00342 } 00343 if (!xdgTargetDirectory.isEmpty()) { 00344 installdir = KStandardDirs().localxdgdatadir() + '/' + xdgTargetDirectory + '/'; 00345 pathcounter++; 00346 } 00347 if (!installPath.isEmpty()) { 00348 #if defined(Q_WS_WIN) 00349 #ifndef _WIN32_WCE 00350 WCHAR wPath[MAX_PATH+1]; 00351 if ( SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, wPath) == S_OK) { 00352 installdir = QString::fromUtf16((const ushort *) wPath) + QLatin1Char('/') + installpath + QLatin1Char('/'); 00353 } else { 00354 #endif 00355 installdir = QDir::home().path() + QLatin1Char('/') + installPath + QLatin1Char('/'); 00356 #ifndef _WIN32_WCE 00357 } 00358 #endif 00359 #else 00360 installdir = QDir::home().path() + '/' + installPath + '/'; 00361 #endif 00362 pathcounter++; 00363 } 00364 if (!absoluteInstallPath.isEmpty()) { 00365 installdir = absoluteInstallPath + '/'; 00366 pathcounter++; 00367 } 00368 if (pathcounter != 1) { 00369 kError() << "Wrong number of installation directories given." << endl; 00370 return QString(); 00371 } 00372 00373 kDebug() << "installdir: " << installdir; 00374 00375 } 00376 00377 return installdir; 00378 } 00379 00380 QStringList Installation::installDownloadedFileAndUncompress(const KNS3::EntryInternal& entry, const QString& payloadfile, const QString installdir) 00381 { 00382 QString installpath(payloadfile); 00383 // Collect all files that were installed 00384 QStringList installedFiles; 00385 00386 if (!isRemote()) { 00387 bool isarchive = true; 00388 00389 // respect the uncompress flag in the knsrc 00390 if (uncompression == "always" || uncompression == "archive") { 00391 // this is weird but a decompression is not a single name, so take the path instead 00392 installpath = installdir; 00393 KMimeType::Ptr mimeType = KMimeType::findByPath(payloadfile); 00394 //kDebug() << "Postinstallation: uncompress the file"; 00395 00396 // FIXME: check for overwriting, malicious archive entries (../foo) etc. 00397 // FIXME: KArchive should provide "safe mode" for this! 00398 KArchive *archive = 0; 00399 00400 00401 if (mimeType->is("application/zip")) { 00402 archive = new KZip(payloadfile); 00403 } else if (mimeType->is("application/tar") 00404 || mimeType->is("application/x-gzip") 00405 || mimeType->is("application/x-bzip") 00406 || mimeType->is("application/x-lzma") 00407 || mimeType->is("application/x-xz") 00408 || mimeType->is("application/x-bzip-compressed-tar") 00409 || mimeType->is("application/x-compressed-tar") ) { 00410 archive = new KTar(payloadfile); 00411 } else { 00412 delete archive; 00413 kError() << "Could not determine type of archive file '" << payloadfile << "'"; 00414 if (uncompression == "always") { 00415 return QStringList(); 00416 } 00417 isarchive = false; 00418 } 00419 00420 if (isarchive) { 00421 bool success = archive->open(QIODevice::ReadOnly); 00422 if (!success) { 00423 kError() << "Cannot open archive file '" << payloadfile << "'"; 00424 if (uncompression == "always") { 00425 return QStringList(); 00426 } 00427 // otherwise, just copy the file 00428 isarchive = false; 00429 } 00430 00431 if (isarchive) { 00432 const KArchiveDirectory *dir = archive->directory(); 00433 dir->copyTo(installdir); 00434 00435 installedFiles << archiveEntries(installdir, dir); 00436 installedFiles << installdir + '/'; 00437 00438 archive->close(); 00439 QFile::remove(payloadfile); 00440 delete archive; 00441 } 00442 } 00443 } 00444 00445 kDebug() << "isarchive: " << isarchive; 00446 00447 if (uncompression == "never" || (uncompression == "archive" && !isarchive)) { 00448 // no decompress but move to target 00449 00451 // FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names 00452 KUrl source = KUrl(entry.payload()); 00453 kDebug() << "installing non-archive from " << source.url(); 00454 QString installfile; 00455 QString ext = source.fileName().section('.', -1); 00456 if (customName) { 00457 installfile = entry.name(); 00458 installfile += '-' + entry.version(); 00459 if (!ext.isEmpty()) installfile += '.' + ext; 00460 } else { 00461 installfile = source.fileName(); 00462 } 00463 installpath = installdir + '/' + installfile; 00464 00465 //kDebug() << "Install to file " << installpath; 00466 // FIXME: copy goes here (including overwrite checking) 00467 // FIXME: what must be done now is to update the cache *again* 00468 // in order to set the new payload filename (on root tag only) 00469 // - this might or might not need to take uncompression into account 00470 // FIXME: for updates, we might need to force an overwrite (that is, deleting before) 00471 QFile file(payloadfile); 00472 bool success = true; 00473 const bool update = ((entry.status() == Entry::Updateable) || (entry.status() == Entry::Updating)); 00474 00475 if (QFile::exists(installpath)) { 00476 if (!update) { 00477 if (KMessageBox::warningContinueCancel(0, i18n("Overwrite existing file?") + "\n'" + installpath + '\'', i18n("Download File:")) == KMessageBox::Cancel) { 00478 return QStringList(); 00479 } 00480 } 00481 success = QFile::remove(installpath); 00482 } 00483 if (success) { 00484 success = file.rename(KUrl(installpath).toLocalFile()); 00485 kDebug() << "move: " << file.fileName() << " to " << installpath; 00486 } 00487 if (!success) { 00488 kError() << "Cannot move file '" << payloadfile << "' to destination '" << installpath << "'"; 00489 return QStringList(); 00490 } 00491 installedFiles << installpath; 00492 } 00493 } 00494 return installedFiles; 00495 } 00496 00497 void Installation::runPostInstallationCommand(const QString& installPath) 00498 { 00499 KProcess process; 00500 QString command(postInstallationCommand); 00501 QString fileArg(KShell::quoteArg(installPath)); 00502 command.replace("%f", fileArg); 00503 00504 kDebug() << "Run command: " << command; 00505 00506 process.setShellCommand(command); 00507 int exitcode = process.execute(); 00508 00509 if (exitcode) { 00510 kError() << "Command failed" << endl; 00511 } 00512 } 00513 00514 00515 void Installation::uninstall(EntryInternal entry) 00516 { 00517 entry.setStatus(Entry::Deleted); 00518 00519 if (!uninstallCommand.isEmpty()) { 00520 KProcess process; 00521 foreach (const QString& file, entry.installedFiles()) { 00522 QFileInfo info(file); 00523 if (info.isFile()) { 00524 QString fileArg(KShell::quoteArg(file)); 00525 QString command(uninstallCommand); 00526 command.replace("%f", fileArg); 00527 00528 process.setShellCommand(command); 00529 int exitcode = process.execute(); 00530 00531 if (exitcode) { 00532 kError() << "Command failed" << endl; 00533 } else { 00534 //kDebug() << "Command executed successfully"; 00535 } 00536 } 00537 } 00538 } 00539 00540 foreach(const QString &file, entry.installedFiles()) { 00541 if (file.endsWith('/')) { 00542 QDir dir; 00543 bool worked = dir.rmdir(file); 00544 if (!worked) { 00545 // Maybe directory contains user created files, ignore it 00546 continue; 00547 } 00548 } else { 00549 if (QFile::exists(file)) { 00550 bool worked = QFile::remove(file); 00551 if (!worked) { 00552 kWarning() << "unable to delete file " << file; 00553 return; 00554 } 00555 } else { 00556 kWarning() << "unable to delete file " << file << ". file does not exist."; 00557 } 00558 } 00559 } 00560 entry.setUnInstalledFiles(entry.installedFiles()); 00561 entry.setInstalledFiles(QStringList()); 00562 00563 emit signalEntryChanged(entry); 00564 } 00565 00566 00567 void Installation::slotInstallationVerification(int result) 00568 { 00569 //kDebug() << "SECURITY result " << result; 00570 00571 //FIXME do something here ??? and get the right entry again 00572 EntryInternal entry; 00573 00574 if (result & Security::SIGNED_OK) 00575 emit signalEntryChanged(entry); 00576 else 00577 emit signalEntryChanged(entry); 00578 } 00579 00580 00581 QStringList Installation::archiveEntries(const QString& path, const KArchiveDirectory * dir) 00582 { 00583 QStringList files; 00584 foreach(const QString &entry, dir->entries()) { 00585 QString childPath = path + '/' + entry; 00586 if (dir->entry(entry)->isFile()) { 00587 files << childPath; 00588 } 00589 00590 if (dir->entry(entry)->isDirectory()) { 00591 const KArchiveDirectory* childDir = static_cast<const KArchiveDirectory*>(dir->entry(entry)); 00592 files << archiveEntries(childPath, childDir); 00593 files << childPath + '/'; 00594 } 00595 } 00596 return files; 00597 } 00598 00599 00600 #include "installation.moc"
KDE 4.6 API Reference