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

KParts

  • kparts
browserrun.cpp
Go to the documentation of this file.
1 /* This file is part of the KDE project
2  *
3  * Copyright (C) 2002 David Faure <faure@kde.org>
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License version 2, as published by the Free Software Foundation.
7  *
8  * This library is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11  * Library General Public License for more details.
12  *
13  * You should have received a copy of the GNU Library General Public License
14  * along with this library; see the file COPYING.LIB. If not, write to
15  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16  * Boston, MA 02110-1301, USA.
17  */
18 
19 #include "browserrun.h"
20 #include "browserrun_p.h"
21 
22 #include <kmessagebox.h>
23 #include <kfiledialog.h>
24 #include <kio/job.h>
25 #include <kio/jobuidelegate.h>
26 #include <kio/scheduler.h>
27 #include <kio/copyjob.h>
28 #include <klocale.h>
29 #include <kshell.h>
30 #include <kstringhandler.h>
31 #include <kmimetypetrader.h>
32 #include <ktemporaryfile.h>
33 #include <kdebug.h>
34 #include <kde_file.h>
35 #include <kstandarddirs.h>
36 #include <kdatetime.h>
37 #include "browseropenorsavequestion.h"
38 #include <kprotocolmanager.h>
39 
40 using namespace KParts;
41 
42 class BrowserRun::BrowserRunPrivate
43 {
44 public:
45  bool m_bHideErrorDialog;
46  bool m_bRemoveReferrer;
47  bool m_bTrustedSource;
48  KParts::OpenUrlArguments m_args;
49  KParts::BrowserArguments m_browserArgs;
50 
51  KParts::ReadOnlyPart *m_part; // QGuardedPtr?
52  QPointer<QWidget> m_window;
53  QString m_mimeType;
54  QString m_contentDisposition;
55 };
56 
57 BrowserRun::BrowserRun( const KUrl& url, const KParts::OpenUrlArguments& args,
58  const KParts::BrowserArguments& browserArgs,
59  KParts::ReadOnlyPart *part, QWidget* window,
60  bool removeReferrer, bool trustedSource, bool hideErrorDialog )
61  : KRun( url, window, 0 /*mode*/, false /*is_local_file known*/, false /* no GUI */ ),
62  d(new BrowserRunPrivate)
63 {
64  d->m_bHideErrorDialog = hideErrorDialog;
65  d->m_bRemoveReferrer = removeReferrer;
66  d->m_bTrustedSource = trustedSource;
67  d->m_args = args;
68  d->m_browserArgs = browserArgs;
69  d->m_part = part;
70  d->m_window = window;
71 }
72 
73 BrowserRun::~BrowserRun()
74 {
75  delete d;
76 }
77 
78 KParts::ReadOnlyPart* BrowserRun::part() const
79 {
80  return d->m_part;
81 }
82 
83 KUrl BrowserRun::url() const
84 {
85  return KRun::url();
86 }
87 
88 void BrowserRun::init()
89 {
90  if ( d->m_bHideErrorDialog )
91  {
92  // ### KRun doesn't call a virtual method when it finds out that the URL
93  // is either malformed, or points to a non-existing local file...
94  // So we need to reimplement some of the checks, to handle d->m_bHideErrorDialog
95  if ( !KRun::url().isValid() ) {
96  redirectToError( KIO::ERR_MALFORMED_URL, KRun::url().url() );
97  return;
98  }
99  if ( !isLocalFile() && !hasError() && KRun::url().isLocalFile() )
100  setIsLocalFile( true );
101 
102  if ( isLocalFile() ) {
103  KDE_struct_stat buff;
104  if ( KDE::stat( KRun::url().toLocalFile(), &buff ) == -1 )
105  {
106  kDebug(1000) << KRun::url().toLocalFile() << "doesn't exist.";
107  redirectToError( KIO::ERR_DOES_NOT_EXIST, KRun::url().toLocalFile() );
108  return;
109  }
110  setMode( buff.st_mode ); // while we're at it, save it for KRun::init() to use it
111  }
112  }
113  KRun::init();
114 }
115 
116 void BrowserRun::scanFile()
117 {
118  kDebug(1000) << KRun::url();
119 
120  // Let's check for well-known extensions
121  // Not when there is a query in the URL, in any case.
122  // Optimization for http/https, findByURL doesn't trust extensions over http.
123  QString protocol = KRun::url().protocol();
124 
125  if (!KProtocolInfo::proxiedBy(protocol).isEmpty()) {
126  QString dummy;
127  protocol = KProtocolManager::slaveProtocol(KRun::url(), dummy);
128  }
129 
130  if ( KRun::url().query().isEmpty() && !protocol.startsWith(QLatin1String("http")))
131  {
132  KMimeType::Ptr mime = KMimeType::findByUrl( KRun::url() );
133  Q_ASSERT( mime );
134  if ( !mime->isDefault() || isLocalFile() )
135  {
136  kDebug(1000) << "MIME TYPE is" << mime->name();
137  mimeTypeDetermined( mime->name() );
138  return;
139  }
140  }
141 
142  QMap<QString, QString>& metaData = d->m_args.metaData();
143  if ( d->m_part ) {
144  const QString proto = d->m_part->url().protocol().toLower();
145 
146  if (proto == "https" || proto == "webdavs") {
147  metaData.insert("main_frame_request", "TRUE" );
148  metaData.insert("ssl_was_in_use", "TRUE" );
149  // metaData.insert("ssl_activate_warnings", "TRUE" );
150  } else if (proto == "http" || proto == "webdav") {
151  // metaData.insert("ssl_activate_warnings", "TRUE" );
152  metaData.insert("ssl_was_in_use", "FALSE" );
153  }
154 
155  // Set the PropagateHttpHeader meta-data if it has not already been set...
156  if (!metaData.contains("PropagateHttpHeader"))
157  metaData.insert("PropagateHttpHeader", "TRUE");
158  }
159 
160  KIO::TransferJob *job;
161  if ( d->m_browserArgs.doPost() && KRun::url().protocol().startsWith(QLatin1String("http"))) {
162  job = KIO::http_post( KRun::url(), d->m_browserArgs.postData, KIO::HideProgressInfo );
163  job->addMetaData( "content-type", d->m_browserArgs.contentType() );
164  } else {
165  job = KIO::get(KRun::url(),
166  d->m_args.reload() ? KIO::Reload : KIO::NoReload,
167  KIO::HideProgressInfo);
168  }
169 
170  if ( d->m_bRemoveReferrer )
171  metaData.remove("referrer");
172 
173  job->addMetaData( metaData );
174  job->ui()->setWindow( d->m_window );
175  connect( job, SIGNAL(result(KJob*)),
176  this, SLOT(slotBrowserScanFinished(KJob*)));
177  connect( job, SIGNAL(mimetype(KIO::Job*,QString)),
178  this, SLOT(slotBrowserMimetype(KIO::Job*,QString)));
179  setJob( job );
180 }
181 
182 void BrowserRun::slotBrowserScanFinished(KJob *job)
183 {
184  kDebug(1000) << job->error();
185  if ( job->error() == KIO::ERR_IS_DIRECTORY )
186  {
187  // It is in fact a directory. This happens when HTTP redirects to FTP.
188  // Due to the "protocol doesn't support listing" code in BrowserRun, we
189  // assumed it was a file.
190  kDebug(1000) << "It is in fact a directory!";
191  // Update our URL in case of a redirection
192  KRun::setUrl( static_cast<KIO::TransferJob *>(job)->url() );
193  setJob( 0 );
194  mimeTypeDetermined( "inode/directory" );
195  }
196  else
197  {
198  if ( job->error() )
199  handleError( job );
200  else
201  KRun::slotScanFinished(job);
202  }
203 }
204 
205 void BrowserRun::slotBrowserMimetype( KIO::Job *_job, const QString &type )
206 {
207  Q_ASSERT( _job == KRun::job() ); Q_UNUSED(_job)
208  KIO::TransferJob *job = static_cast<KIO::TransferJob *>(KRun::job());
209  // Update our URL in case of a redirection
210  //kDebug(1000) << "old URL=" << KRun::url();
211  //kDebug(1000) << "new URL=" << job->url();
212  setUrl( job->url() );
213 
214  if (job->isErrorPage()) {
215  d->m_mimeType = type;
216  handleError(job);
217  setJob( 0 );
218  } else {
219  kDebug(1000) << "found" << type << "for" << KRun::url();
220 
221  // Suggested filename given by the server (e.g. HTTP content-disposition)
222  // When set, we should really be saving instead of embedding
223  const QString suggestedFileName = job->queryMetaData("content-disposition-filename");
224  setSuggestedFileName(suggestedFileName); // store it (in KRun)
225  //kDebug(1000) << "suggestedFileName=" << suggestedFileName;
226  d->m_contentDisposition = job->queryMetaData("content-disposition-type");
227 
228  const QString modificationTime = job->queryMetaData("content-disposition-modification-date");
229  if (!modificationTime.isEmpty()) {
230  d->m_args.metaData().insert(QLatin1String("content-disposition-modification-date"), modificationTime);
231  }
232 
233  QMapIterator<QString,QString> it (job->metaData());
234  while (it.hasNext()) {
235  it.next();
236  if (it.key().startsWith(QLatin1String("ssl_"), Qt::CaseInsensitive))
237  d->m_args.metaData().insert(it.key(), it.value());
238  }
239 
240  // Make a copy to avoid a dead reference
241  QString _type = type;
242  job->putOnHold();
243  setJob( 0 );
244 
245  mimeTypeDetermined( _type );
246  }
247 }
248 
249 BrowserRun::NonEmbeddableResult BrowserRun::handleNonEmbeddable(const QString& mimeType)
250 {
251  return handleNonEmbeddable(mimeType, NULL);
252 }
253 
254 BrowserRun::NonEmbeddableResult BrowserRun::handleNonEmbeddable(const QString& _mimeType, KService::Ptr* selectedService)
255 {
256  QString mimeType( _mimeType );
257  Q_ASSERT( !hasFinished() ); // only come here if the mimetype couldn't be embedded
258  // Support for saving remote files.
259  if ( mimeType != "inode/directory" && // dirs can't be saved
260  !KRun::url().isLocalFile() )
261  {
262  if ( isTextExecutable(mimeType) )
263  mimeType = QLatin1String("text/plain"); // view, don't execute
264  // ... -> ask whether to save
265  BrowserOpenOrSaveQuestion question(d->m_window, KRun::url(), mimeType);
266  question.setSuggestedFileName(suggestedFileName());
267  if (selectedService)
268  question.setFeatures(BrowserOpenOrSaveQuestion::ServiceSelection);
269  BrowserOpenOrSaveQuestion::Result res = question.askOpenOrSave();
270  if (res == BrowserOpenOrSaveQuestion::Save) {
271  save( KRun::url(), suggestedFileName() );
272  kDebug(1000) << "Save: returning Handled";
273  setFinished( true );
274  return Handled;
275  }
276  else if (res == BrowserOpenOrSaveQuestion::Cancel) {
277  // saving done or canceled
278  kDebug(1000) << "Cancel: returning Handled";
279  setFinished( true );
280  return Handled;
281  }
282  else // "Open" chosen (done by KRun::foundMimeType, called when returning NotHandled)
283  {
284  // If we were in a POST, we can't just pass a URL to an external application.
285  // We must save the data to a tempfile first.
286  if ( d->m_browserArgs.doPost() )
287  {
288  kDebug(1000) << "request comes from a POST, can't pass a URL to another app, need to save";
289  d->m_mimeType = mimeType;
290  QString extension;
291  QString fileName = suggestedFileName().isEmpty() ? KRun::url().fileName() : suggestedFileName();
292  int extensionPos = fileName.lastIndexOf( '.' );
293  if ( extensionPos != -1 )
294  extension = fileName.mid( extensionPos ); // keep the '.'
295  KTemporaryFile tempFile;
296  tempFile.setSuffix(extension);
297  tempFile.setAutoRemove(false);
298  tempFile.open();
299  KUrl destURL;
300  destURL.setPath( tempFile.fileName() );
301  KIO::Job *job = KIO::file_copy( KRun::url(), destURL, 0600, KIO::Overwrite );
302  job->ui()->setWindow(d->m_window);
303  connect( job, SIGNAL(result(KJob*)),
304  this, SLOT(slotCopyToTempFileResult(KJob*)) );
305  return Delayed; // We'll continue after the job has finished
306  }
307  if (selectedService && question.selectedService()) {
308  *selectedService = question.selectedService();
309  // KRun will use this when starting an app
310  KRun::setPreferredService(question.selectedService()->desktopEntryName());
311  }
312  }
313  }
314 
315  // Check if running is allowed
316  if ( !d->m_bTrustedSource && // ... and untrusted source...
317  !allowExecution( mimeType, KRun::url() ) ) // ...and the user said no (for executables etc.)
318  {
319  setFinished( true );
320  return Handled;
321  }
322 
323  KIO::Scheduler::publishSlaveOnHold(); // publish any slave on hold so it can be reused.
324  return NotHandled;
325 }
326 
327 //static
328 bool BrowserRun::allowExecution( const QString &mimeType, const KUrl &url )
329 {
330  if ( !KRun::isExecutable( mimeType ) )
331  return true;
332 
333  if ( !url.isLocalFile() ) // Don't permit to execute remote files
334  return false;
335 
336  return ( KMessageBox::warningContinueCancel( 0,
337  i18n( "Do you really want to execute '%1'?", url.prettyUrl() ),
338  i18n("Execute File?"), KGuiItem(i18n("Execute")) ) == KMessageBox::Continue );
339 }
340 
341 //static, deprecated
342 #ifndef KDE_NO_DEPRECATED
343 BrowserRun::AskSaveResult BrowserRun::askSave( const KUrl & url, KService::Ptr offer, const QString& mimeType, const QString & suggestedFileName )
344 {
345  Q_UNUSED(offer);
346  BrowserOpenOrSaveQuestion question(0, url, mimeType);
347  question.setSuggestedFileName(suggestedFileName);
348  const BrowserOpenOrSaveQuestion::Result result = question.askOpenOrSave();
349  return result == BrowserOpenOrSaveQuestion::Save ? Save
350  : BrowserOpenOrSaveQuestion::Open ? Open
351  : Cancel;
352 }
353 #endif
354 
355 //static, deprecated
356 #ifndef KDE_NO_DEPRECATED
357 BrowserRun::AskSaveResult BrowserRun::askEmbedOrSave( const KUrl & url, const QString& mimeType, const QString & suggestedFileName, int flags )
358 {
359  BrowserOpenOrSaveQuestion question(0, url, mimeType);
360  question.setSuggestedFileName(suggestedFileName);
361  const BrowserOpenOrSaveQuestion::Result result = question.askEmbedOrSave(flags);
362  return result == BrowserOpenOrSaveQuestion::Save ? Save
363  : BrowserOpenOrSaveQuestion::Embed ? Open
364  : Cancel;
365 }
366 #endif
367 
368 // Default implementation, overridden in KHTMLRun
369 void BrowserRun::save( const KUrl & url, const QString & suggestedFileName )
370 {
371  saveUrl(url, suggestedFileName, d->m_window, d->m_args);
372 }
373 
374 // static
375 void BrowserRun::simpleSave( const KUrl & url, const QString & suggestedFileName,
376  QWidget* window )
377 {
378  saveUrl(url, suggestedFileName, window, KParts::OpenUrlArguments());
379 }
380 
381 void KParts::BrowserRun::saveUrl(const KUrl & url, const QString & suggestedFileName,
382  QWidget* window, const KParts::OpenUrlArguments& args)
383 {
384  // DownloadManager <-> konqueror integration
385  // find if the integration is enabled
386  // the empty key means no integration
387  // only use the downloadmanager for non-local urls
388  if ( !url.isLocalFile() )
389  {
390  KConfigGroup cfg = KSharedConfig::openConfig("konquerorrc", KConfig::NoGlobals)->group("HTML Settings");
391  QString downloadManger = cfg.readPathEntry("DownloadManager", QString());
392  if (!downloadManger.isEmpty())
393  {
394  // then find the download manager location
395  kDebug(1000) << "Using: "<<downloadManger <<" as Download Manager";
396  QString cmd=KStandardDirs::findExe(downloadManger);
397  if (cmd.isEmpty())
398  {
399  QString errMsg=i18n("The Download Manager (%1) could not be found in your $PATH ", downloadManger);
400  QString errMsgEx= i18n("Try to reinstall it \n\nThe integration with Konqueror will be disabled.");
401  KMessageBox::detailedSorry(0,errMsg,errMsgEx);
402  cfg.writePathEntry("DownloadManager",QString());
403  cfg.sync ();
404  }
405  else
406  {
407  // ### suggestedFileName not taken into account. Fix this (and
408  // the duplicated code) with shiny new KDownload class for 3.2 (pfeiffer)
409  // Until the shiny new class comes about, send the suggestedFileName
410  // along with the actual URL to download. (DA)
411  cmd += ' ' + KShell::quoteArg(url.url());
412  if ( !suggestedFileName.isEmpty() )
413  cmd += ' ' + KShell::quoteArg(suggestedFileName);
414 
415  kDebug(1000) << "Calling command" << cmd;
416  // slave is already on hold (slotBrowserMimetype())
417  KIO::Scheduler::publishSlaveOnHold();
418  KRun::runCommand(cmd, window);
419  return;
420  }
421  }
422  }
423 
424  // no download manager available, let's do it ourself
425  KFileDialog *dlg = new KFileDialog( QString(), QString() /*all files*/,
426  window);
427  dlg->setOperationMode( KFileDialog::Saving );
428  dlg->setCaption(i18n("Save As"));
429  dlg->setConfirmOverwrite(true);
430 
431  QString name;
432  if ( !suggestedFileName.isEmpty() )
433  name = suggestedFileName;
434  else
435  name = url.fileName(KUrl::ObeyTrailingSlash); // can be empty, e.g. in case http://www.kde.org/
436 
437  dlg->setSelection(name);
438  if ( dlg->exec() )
439  {
440  KUrl destURL( dlg->selectedUrl() );
441  if ( destURL.isValid() )
442  {
443  saveUrlUsingKIO(url, destURL, window, args.metaData());
444  }
445  }
446  delete dlg;
447 }
448 
449 void BrowserRun::saveUrlUsingKIO(const KUrl & srcUrl, const KUrl& destUrl,
450  QWidget* window, const QMap<QString, QString> &metaData)
451 {
452  KIO::FileCopyJob *job = KIO::file_copy(srcUrl, destUrl, -1, KIO::Overwrite);
453 
454  const QString modificationTime = metaData[QLatin1String("content-disposition-modification-date")];
455  if (!modificationTime.isEmpty()) {
456  job->setModificationTime(KDateTime::fromString(modificationTime, KDateTime::RFCDate).dateTime());
457  }
458  job->setMetaData(metaData);
459  job->addMetaData("MaxCacheSize", "0"); // Don't store in http cache.
460  job->addMetaData("cache", "cache"); // Use entry from cache if available.
461  job->ui()->setWindow(window);
462  job->ui()->setAutoErrorHandlingEnabled( true );
463  new DownloadJobWatcher(job, metaData);
464 }
465 
466 void BrowserRun::slotStatResult( KJob *job )
467 {
468  if ( job->error() ) {
469  kDebug(1000) << job->errorString();
470  handleError( job );
471  } else
472  KRun::slotStatResult( job );
473 }
474 
475 void BrowserRun::handleError( KJob * job )
476 {
477  if ( !job ) { // Shouldn't happen, see docu.
478  kWarning(1000) << "handleError called with job=0! hideErrorDialog=" << d->m_bHideErrorDialog;
479  return;
480  }
481 
482  KIO::TransferJob *tjob = qobject_cast<KIO::TransferJob *>(job);
483  if (tjob && tjob->isErrorPage() && !job->error()) {
484  // The default handling of error pages is to show them like normal pages
485  // But this is done here in handleError so that KHTMLRun can reimplement it
486  tjob->putOnHold();
487  setJob(0);
488  if (!d->m_mimeType.isEmpty())
489  mimeTypeDetermined(d->m_mimeType);
490  return;
491  }
492 
493  if (d->m_bHideErrorDialog && job->error() != KIO::ERR_NO_CONTENT)
494  {
495  redirectToError( job->error(), job->errorText() );
496  return;
497  }
498 
499  // Reuse code in KRun, to benefit from d->m_showingError etc.
500  KRun::slotStatResult( job );
501 }
502 
503 // static
504 KUrl BrowserRun::makeErrorUrl(int error, const QString& errorText, const QString& initialUrl)
505 {
506  /*
507  * The format of the error:/ URL is error:/?query#url,
508  * where two variables are passed in the query:
509  * error = int kio error code, errText = QString error text from kio
510  * The sub-url is the URL that we were trying to open.
511  */
512  KUrl newURL(QString("error:/?error=%1&errText=%2")
513  .arg( error )
514  .arg( QString::fromUtf8( QUrl::toPercentEncoding( errorText ) ) ) );
515 
516  QString cleanedOrigUrl = initialUrl;
517  KUrl runURL = cleanedOrigUrl;
518  if (runURL.isValid()) {
519  runURL.setPass( QString() ); // don't put the password in the error URL
520  cleanedOrigUrl = runURL.url();
521  }
522 
523  newURL.setFragment(cleanedOrigUrl);
524  return newURL;
525 
526  // The kde3 approach broke with invalid urls, now that they become empty in qt4.
527  //KUrl::List lst;
528  //lst << newURL << runURL;
529  //return KUrl::join(lst);
530 }
531 
532 void BrowserRun::redirectToError( int error, const QString& errorText )
533 {
539  KRun::setUrl(makeErrorUrl(error, errorText, url().url()));
540  setJob( 0 );
541  mimeTypeDetermined( "text/html" );
542 }
543 
544 void BrowserRun::slotCopyToTempFileResult(KJob *job)
545 {
546  if ( job->error() ) {
547  job->uiDelegate()->showErrorMessage();
548  } else {
549  // Same as KRun::foundMimeType but with a different URL
550  (void) (KRun::runUrl( static_cast<KIO::FileCopyJob *>(job)->destUrl(), d->m_mimeType, d->m_window ));
551  }
552  setError( true ); // see above
553  setFinished( true );
554 }
555 
556 bool BrowserRun::isTextExecutable( const QString &mimeType )
557 {
558  return ( mimeType == "application/x-desktop" ||
559  mimeType == "application/x-shellscript" );
560 }
561 
562 bool BrowserRun::hideErrorDialog() const
563 {
564  return d->m_bHideErrorDialog;
565 }
566 
567 QString BrowserRun::contentDisposition() const
568 {
569  return d->m_contentDisposition;
570 }
571 
572 bool BrowserRun::serverSuggestsSave() const
573 {
574  // RfC 2183, section 2.8:
575  // Unrecognized disposition types should be treated as `attachment'.
576  return !contentDisposition().isEmpty() && (contentDisposition() != "inline");
577 }
578 
579 KParts::OpenUrlArguments& KParts::BrowserRun::arguments()
580 {
581  return d->m_args;
582 }
583 
584 KParts::BrowserArguments& KParts::BrowserRun::browserArguments()
585 {
586  return d->m_browserArgs;
587 }
588 
589 #include "browserrun.moc"
590 #include "browserrun_p.moc"
This file is part of the KDE documentation.
Documentation copyright © 1996-2012 The KDE developers.
Generated on Mon Dec 10 2012 13:59:34 by doxygen 1.8.1.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

KParts

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

kdelibs-4.9.4 API Reference

Skip menu "kdelibs-4.9.4 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