• Skip to content
  • Skip to link menu
  • KDE API Reference
  • kdelibs-4.9.5 API Reference
  • KDE Home
  • Contact Us
 

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                 // TODO HACK This is a hack, the correct way of fixing it would be doing the KIO::get
00462                 // and using the http headers if they exist to get the file name, but as discussed in
00463                 // Randa this is not going to happen anytime soon (if ever) so go with the hack
00464                 if (source.url().startsWith("http://newstuff.kde.org/cgi-bin/hotstuff-access?file=")) {
00465                     installfile = source.queryItemValue("file");
00466                     int lastSlash = installfile.lastIndexOf('/');
00467                     if (lastSlash >= 0)
00468                         installfile = installfile.mid(lastSlash);
00469                 }
00470                 if (installfile.isEmpty()) {
00471                     installfile = source.fileName();
00472                 }
00473             }
00474             installpath = installdir + '/' + installfile;
00475 
00476             //kDebug() << "Install to file " << installpath;
00477             // FIXME: copy goes here (including overwrite checking)
00478             // FIXME: what must be done now is to update the cache *again*
00479             //        in order to set the new payload filename (on root tag only)
00480             //        - this might or might not need to take uncompression into account
00481             // FIXME: for updates, we might need to force an overwrite (that is, deleting before)
00482             QFile file(payloadfile);
00483             bool success = true;
00484             const bool update = ((entry.status() == Entry::Updateable) || (entry.status() == Entry::Updating));
00485 
00486             if (QFile::exists(installpath)) {
00487                 if (!update) {
00488                     if (KMessageBox::warningContinueCancel(0, i18n("Overwrite existing file?") + "\n'" + installpath + '\'', i18n("Download File:")) == KMessageBox::Cancel) {
00489                         return QStringList();
00490                     }
00491                 }
00492                 success = QFile::remove(installpath);
00493             }
00494             if (success) {
00495                 success = file.rename(KUrl(installpath).toLocalFile());
00496                 kDebug() << "move: " << file.fileName() << " to " << installpath;
00497             }
00498             if (!success) {
00499                 kError() << "Cannot move file '" << payloadfile << "' to destination '"  << installpath << "'";
00500                 return QStringList();
00501             }
00502             installedFiles << installpath;
00503         }
00504     }
00505     return installedFiles;
00506 }
00507 
00508 void Installation::runPostInstallationCommand(const QString& installPath)
00509 {
00510     KProcess process;
00511     QString command(postInstallationCommand);
00512     QString fileArg(KShell::quoteArg(installPath));
00513     command.replace("%f", fileArg);
00514 
00515     kDebug() << "Run command: " << command;
00516 
00517     process.setShellCommand(command);
00518     int exitcode = process.execute();
00519 
00520     if (exitcode) {
00521         kError() << "Command failed" << endl;
00522     }
00523 }
00524 
00525 
00526 void Installation::uninstall(EntryInternal entry)
00527 {
00528     entry.setStatus(Entry::Deleted);
00529 
00530     if (!uninstallCommand.isEmpty()) {
00531         KProcess process;
00532         foreach (const QString& file, entry.installedFiles()) {
00533             QFileInfo info(file);
00534             if (info.isFile()) {
00535                 QString fileArg(KShell::quoteArg(file));
00536                 QString command(uninstallCommand);
00537                 command.replace("%f", fileArg);
00538 
00539                 process.setShellCommand(command);
00540                 int exitcode = process.execute();
00541 
00542                 if (exitcode) {
00543                     kError() << "Command failed" << endl;
00544                 } else {
00545                     //kDebug() << "Command executed successfully";
00546                 }
00547             }
00548         }
00549     }
00550 
00551     foreach(const QString &file, entry.installedFiles()) {
00552         if (file.endsWith('/')) {
00553             QDir dir;
00554             bool worked = dir.rmdir(file);
00555             if (!worked) {
00556                 // Maybe directory contains user created files, ignore it
00557                 continue;
00558             }
00559         } else {
00560             QFileInfo info(file);
00561             if (info.exists() || info.isSymLink()) {
00562                 bool worked = QFile::remove(file);
00563                 if (!worked) {
00564                     kWarning() << "unable to delete file " << file;
00565                     return;
00566                 }
00567             } else {
00568                 kWarning() << "unable to delete file " << file << ". file does not exist.";
00569             }
00570         }
00571     }
00572     entry.setUnInstalledFiles(entry.installedFiles());
00573     entry.setInstalledFiles(QStringList());
00574 
00575     emit signalEntryChanged(entry);
00576 }
00577 
00578 
00579 void Installation::slotInstallationVerification(int result)
00580 {
00581     //kDebug() << "SECURITY result " << result;
00582 
00583     //FIXME do something here ??? and get the right entry again
00584     EntryInternal entry;
00585 
00586     if (result & Security::SIGNED_OK)
00587         emit signalEntryChanged(entry);
00588     else
00589         emit signalEntryChanged(entry);
00590 }
00591 
00592 
00593 QStringList Installation::archiveEntries(const QString& path, const KArchiveDirectory * dir)
00594 {
00595     QStringList files;
00596     foreach(const QString &entry, dir->entries()) {
00597         QString childPath = path + '/' + entry;
00598         if (dir->entry(entry)->isFile()) {
00599             files << childPath;
00600         }
00601 
00602         if (dir->entry(entry)->isDirectory()) {
00603             const KArchiveDirectory* childDir = static_cast<const KArchiveDirectory*>(dir->entry(entry));
00604             files << archiveEntries(childPath, childDir);
00605             files << childPath + '/';
00606         }
00607     }
00608     return files;
00609 }
00610 
00611 
00612 #include "installation.moc"
This file is part of the KDE documentation.
Documentation copyright © 1996-2019 The KDE developers.
Generated on Mon Jan 21 2019 12:36:55 by doxygen 1.7.5.1 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

KNewStuff

Skip menu "KNewStuff"
  • Main Page
  • Namespace List
  • Namespace Members
  • Alphabetical List
  • Class List
  • Class Hierarchy
  • Class Members
  • File List
  • File Members
  • Related Pages

kdelibs-4.9.5 API Reference

Skip menu "kdelibs-4.9.5 API Reference"
  • DNSSD
  • Interfaces
  •   KHexEdit
  •   KMediaPlayer
  •   KSpeech
  •   KTextEditor
  • kconf_update
  • KDE3Support
  •   KUnitTest
  • KDECore
  • KDED
  • KDEsu
  • KDEUI
  • KDEWebKit
  • KDocTools
  • KFile
  • KHTML
  • KImgIO
  • KInit
  • kio
  • KIOSlave
  • KJS
  •   KJS-API
  •   WTF
  • kjsembed
  • KNewStuff
  • KParts
  • KPty
  • Kross
  • KUnitConversion
  • KUtils
  • Nepomuk
  • Plasma
  • Solid
  • Sonnet
  • ThreadWeaver
Report problems with this website to our bug tracking system.
Contact the specific authors with questions and comments about the page contents.

KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal