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

KIOSlave

  • kioslave
  • file
file.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2000-2002 Stephan Kulow <coolo@kde.org>
3  Copyright (C) 2000-2002 David Faure <faure@kde.org>
4  Copyright (C) 2000-2002 Waldo Bastian <bastian@kde.org>
5  Copyright (C) 2006 Allan Sandfeld Jensen <sandfeld@kde.org>
6  Copyright (C) 2007 Thiago Macieira <thiago@kde.org>
7 
8  This library is free software; you can redistribute it and/or
9  modify it under the terms of the GNU Library General Public
10  License (LGPL) as published by the Free Software Foundation;
11  either version 2 of the License, or (at your option) any later
12  version.
13 
14  This library is distributed in the hope that it will be useful,
15  but WITHOUT ANY WARRANTY; without even the implied warranty of
16  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17  Library General Public License for more details.
18 
19  You should have received a copy of the GNU Library General Public License
20  along with this library; see the file COPYING.LIB. If not, write to
21  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22  Boston, MA 02110-1301, USA.
23 */
24 
25 #define QT_NO_CAST_FROM_ASCII
26 
27 #include "file.h"
28 #include <QDirIterator>
29 
30 #include <config.h>
31 #include <config-acl.h>
32 #include <config-kioslave-file.h>
33 
34 
35 #include <sys/types.h>
36 #include <sys/wait.h>
37 #include <sys/stat.h>
38 #include <sys/socket.h>
39 #ifdef HAVE_SYS_TIME_H
40 #include <sys/time.h>
41 #endif
42 
43 #ifdef HAVE_POSIX_ACL
44 #include <sys/acl.h>
45 #include <acl/libacl.h>
46 #endif
47 
48 #include <assert.h>
49 #include <dirent.h>
50 #include <errno.h>
51 #include <fcntl.h>
52 #include <grp.h>
53 #include <pwd.h>
54 #include <stdio.h>
55 #include <stdlib.h>
56 #include <signal.h>
57 #include <time.h>
58 #include <utime.h>
59 #include <unistd.h>
60 #ifdef HAVE_STRING_H
61 #include <string.h>
62 #endif
63 
64 #include <QtCore/QByteRef>
65 #include <QtCore/QDate>
66 #include <QtCore/QVarLengthArray>
67 #include <QtCore/QCoreApplication>
68 #include <QtCore/QRegExp>
69 #include <QtCore/QFile>
70 #ifdef Q_WS_WIN
71 #include <QtCore/QDir>
72 #include <QtCore/QFileInfo>
73 #endif
74 
75 #include <kdebug.h>
76 #include <kurl.h>
77 #include <kcomponentdata.h>
78 #include <kconfig.h>
79 #include <kconfiggroup.h>
80 #include <ktemporaryfile.h>
81 #include <klocale.h>
82 #include <limits.h>
83 #include <kshell.h>
84 #include <kmountpoint.h>
85 #include <kstandarddirs.h>
86 
87 #ifdef HAVE_VOLMGT
88 #include <volmgt.h>
89 #include <sys/mnttab.h>
90 #endif
91 
92 #include <kdirnotify.h>
93 #include <kio/ioslave_defaults.h>
94 #include <kde_file.h>
95 #include <kglobal.h>
96 #include <kmimetype.h>
97 
98 using namespace KIO;
99 
100 #define MAX_IPC_SIZE (1024*32)
101 
102 static QString testLogFile( const QByteArray&_filename );
103 #ifdef HAVE_POSIX_ACL
104 static bool isExtendedACL( acl_t p_acl );
105 static void appendACLAtoms( const QByteArray & path, UDSEntry& entry,
106  mode_t type, bool withACL );
107 #endif
108 
109 extern "C" int KDE_EXPORT kdemain( int argc, char **argv )
110 {
111  QCoreApplication app( argc, argv ); // needed for QSocketNotifier
112  KComponentData componentData( "kio_file", "kdelibs4" );
113  ( void ) KGlobal::locale();
114 
115  kDebug(7101) << "Starting" << getpid();
116 
117  if (argc != 4)
118  {
119  fprintf(stderr, "Usage: kio_file protocol domain-socket1 domain-socket2\n");
120  exit(-1);
121  }
122 
123  FileProtocol slave(argv[2], argv[3]);
124  slave.dispatchLoop();
125 
126  kDebug(7101) << "Done";
127  return 0;
128 }
129 
130 FileProtocol::FileProtocol( const QByteArray &pool, const QByteArray &app )
131  : SlaveBase( "file", pool, app ), openFd(-1)
132 {
133 }
134 
135 FileProtocol::~FileProtocol()
136 {
137 }
138 
139 #ifdef HAVE_POSIX_ACL
140 static QString aclToText(acl_t acl) {
141  ssize_t size = 0;
142  char* txt = acl_to_text(acl, &size);
143  const QString ret = QString::fromLatin1(txt, size);
144  acl_free(txt);
145  return ret;
146 }
147 #endif
148 
149 int FileProtocol::setACL( const char *path, mode_t perm, bool directoryDefault )
150 {
151  int ret = 0;
152 #ifdef HAVE_POSIX_ACL
153 
154  const QString ACLString = metaData(QLatin1String("ACL_STRING"));
155  const QString defaultACLString = metaData(QLatin1String("DEFAULT_ACL_STRING"));
156  // Empty strings mean leave as is
157  if ( !ACLString.isEmpty() ) {
158  acl_t acl = 0;
159  if (ACLString == QLatin1String("ACL_DELETE")) {
160  // user told us to delete the extended ACL, so let's write only
161  // the minimal (UNIX permission bits) part
162  acl = acl_from_mode( perm );
163  }
164  acl = acl_from_text( ACLString.toLatin1() );
165  if ( acl_valid( acl ) == 0 ) { // let's be safe
166  ret = acl_set_file( path, ACL_TYPE_ACCESS, acl );
167  kDebug(7101) << "Set ACL on:" << path << "to:" << aclToText(acl);
168  }
169  acl_free( acl );
170  if ( ret != 0 ) return ret; // better stop trying right away
171  }
172 
173  if ( directoryDefault && !defaultACLString.isEmpty() ) {
174  if ( defaultACLString == QLatin1String("ACL_DELETE") ) {
175  // user told us to delete the default ACL, do so
176  ret += acl_delete_def_file( path );
177  } else {
178  acl_t acl = acl_from_text( defaultACLString.toLatin1() );
179  if ( acl_valid( acl ) == 0 ) { // let's be safe
180  ret += acl_set_file( path, ACL_TYPE_DEFAULT, acl );
181  kDebug(7101) << "Set Default ACL on:" << path << "to:" << aclToText(acl);
182  }
183  acl_free( acl );
184  }
185  }
186 #else
187  Q_UNUSED(path);
188  Q_UNUSED(perm);
189  Q_UNUSED(directoryDefault);
190 #endif
191  return ret;
192 }
193 
194 void FileProtocol::chmod( const KUrl& url, int permissions )
195 {
196  const QString path(url.toLocalFile());
197  const QByteArray _path( QFile::encodeName(path) );
198  /* FIXME: Should be atomic */
199  if ( KDE::chmod( path, permissions ) == -1 ||
200  ( setACL( _path.data(), permissions, false ) == -1 ) ||
201  /* if not a directory, cannot set default ACLs */
202  ( setACL( _path.data(), permissions, true ) == -1 && errno != ENOTDIR ) ) {
203 
204  switch (errno) {
205  case EPERM:
206  case EACCES:
207  error(KIO::ERR_ACCESS_DENIED, path);
208  break;
209 #if defined(ENOTSUP)
210  case ENOTSUP: // from setACL since chmod can't return ENOTSUP
211  error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Setting ACL for %1", path));
212  break;
213 #endif
214  case ENOSPC:
215  error(KIO::ERR_DISK_FULL, path);
216  break;
217  default:
218  error(KIO::ERR_CANNOT_CHMOD, path);
219  }
220  } else
221  finished();
222 }
223 
224 void FileProtocol::setModificationTime( const KUrl& url, const QDateTime& mtime )
225 {
226  const QString path(url.toLocalFile());
227  KDE_struct_stat statbuf;
228  if (KDE::lstat(path, &statbuf) == 0) {
229  struct utimbuf utbuf;
230  utbuf.actime = statbuf.st_atime; // access time, unchanged
231  utbuf.modtime = mtime.toTime_t(); // modification time
232  if (KDE::utime(path, &utbuf) != 0) {
233  // TODO: errno could be EACCES, EPERM, EROFS
234  error(KIO::ERR_CANNOT_SETTIME, path);
235  } else {
236  finished();
237  }
238  } else {
239  error(KIO::ERR_DOES_NOT_EXIST, path);
240  }
241 }
242 
243 void FileProtocol::mkdir( const KUrl& url, int permissions )
244 {
245  const QString path(url.toLocalFile());
246 
247  kDebug(7101) << path << "permission=" << permissions;
248 
249  // Remove existing file or symlink, if requested (#151851)
250  if (metaData(QLatin1String("overwrite")) == QLatin1String("true"))
251  QFile::remove(path);
252 
253  KDE_struct_stat buff;
254  if ( KDE::lstat( path, &buff ) == -1 ) {
255  if ( KDE::mkdir( path, 0777 /*umask will be applied*/ ) != 0 ) {
256  if ( errno == EACCES ) {
257  error(KIO::ERR_ACCESS_DENIED, path);
258  return;
259  } else if ( errno == ENOSPC ) {
260  error(KIO::ERR_DISK_FULL, path);
261  return;
262  } else {
263  error(KIO::ERR_COULD_NOT_MKDIR, path);
264  return;
265  }
266  } else {
267  if ( permissions != -1 )
268  chmod( url, permissions );
269  else
270  finished();
271  return;
272  }
273  }
274 
275  if ( S_ISDIR( buff.st_mode ) ) {
276  kDebug(7101) << "ERR_DIR_ALREADY_EXIST";
277  error(KIO::ERR_DIR_ALREADY_EXIST, path);
278  return;
279  }
280  error(KIO::ERR_FILE_ALREADY_EXIST, path);
281  return;
282 }
283 
284 void FileProtocol::get( const KUrl& url )
285 {
286  if (!url.isLocalFile()) {
287  KUrl redir(url);
288  redir.setProtocol(config()->readEntry("DefaultRemoteProtocol", "smb"));
289  redirection(redir);
290  finished();
291  return;
292  }
293 
294  const QString path(url.toLocalFile());
295  KDE_struct_stat buff;
296  if ( KDE::stat( path, &buff ) == -1 ) {
297  if ( errno == EACCES )
298  error(KIO::ERR_ACCESS_DENIED, path);
299  else
300  error(KIO::ERR_DOES_NOT_EXIST, path);
301  return;
302  }
303 
304  if ( S_ISDIR( buff.st_mode ) ) {
305  error(KIO::ERR_IS_DIRECTORY, path);
306  return;
307  }
308  if ( !S_ISREG( buff.st_mode ) ) {
309  error(KIO::ERR_CANNOT_OPEN_FOR_READING, path);
310  return;
311  }
312 
313  int fd = KDE::open( path, O_RDONLY);
314  if ( fd < 0 ) {
315  error(KIO::ERR_CANNOT_OPEN_FOR_READING, path);
316  return;
317  }
318 
319 #if HAVE_FADVISE
320  posix_fadvise( fd, 0, 0, POSIX_FADV_SEQUENTIAL);
321 #endif
322 
323  // Determine the mimetype of the file to be retrieved, and emit it.
324  // This is mandatory in all slaves (for KRun/BrowserRun to work)
325  // In real "remote" slaves, this is usually done using findByNameAndContent
326  // after receiving some data. But we don't know how much data the mimemagic rules
327  // need, so for local files, better use findByUrl with localUrl=true.
328  KMimeType::Ptr mt = KMimeType::findByUrl( url, buff.st_mode, true /* local URL */ );
329  emit mimeType( mt->name() );
330  // Emit total size AFTER mimetype
331  totalSize( buff.st_size );
332 
333  KIO::filesize_t processed_size = 0;
334 
335  const QString resumeOffset = metaData(QLatin1String("resume"));
336  if ( !resumeOffset.isEmpty() )
337  {
338  bool ok;
339  KIO::fileoffset_t offset = resumeOffset.toLongLong(&ok);
340  if (ok && (offset > 0) && (offset < buff.st_size))
341  {
342  if (KDE_lseek(fd, offset, SEEK_SET) == offset)
343  {
344  canResume ();
345  processed_size = offset;
346  kDebug(7101) << "Resume offset:" << KIO::number(offset);
347  }
348  }
349  }
350 
351  char buffer[ MAX_IPC_SIZE ];
352  QByteArray array;
353 
354  while( 1 )
355  {
356  int n = ::read( fd, buffer, MAX_IPC_SIZE );
357  if (n == -1)
358  {
359  if (errno == EINTR)
360  continue;
361  error(KIO::ERR_COULD_NOT_READ, path);
362  ::close(fd);
363  return;
364  }
365  if (n == 0)
366  break; // Finished
367 
368  array = QByteArray::fromRawData(buffer, n);
369  data( array );
370  array.clear();
371 
372  processed_size += n;
373  processedSize( processed_size );
374 
375  //kDebug(7101) << "Processed: " << KIO::number (processed_size);
376  }
377 
378  data( QByteArray() );
379 
380  ::close( fd );
381 
382  processedSize( buff.st_size );
383  finished();
384 }
385 
386 int write_all(int fd, const char *buf, size_t len)
387 {
388  while (len > 0)
389  {
390  ssize_t written = write(fd, buf, len);
391  if (written < 0)
392  {
393  if (errno == EINTR)
394  continue;
395  return -1;
396  }
397  buf += written;
398  len -= written;
399  }
400  return 0;
401 }
402 
403 void FileProtocol::open(const KUrl &url, QIODevice::OpenMode mode)
404 {
405  kDebug(7101) << url;
406 
407  openPath = url.toLocalFile();
408  KDE_struct_stat buff;
409  if (KDE::stat(openPath, &buff) == -1) {
410  if ( errno == EACCES )
411  error(KIO::ERR_ACCESS_DENIED, openPath);
412  else
413  error(KIO::ERR_DOES_NOT_EXIST, openPath);
414  return;
415  }
416 
417  if ( S_ISDIR( buff.st_mode ) ) {
418  error(KIO::ERR_IS_DIRECTORY, openPath);
419  return;
420  }
421  if ( !S_ISREG( buff.st_mode ) ) {
422  error(KIO::ERR_CANNOT_OPEN_FOR_READING, openPath);
423  return;
424  }
425 
426  int flags = 0;
427  if (mode & QIODevice::ReadOnly) {
428  if (mode & QIODevice::WriteOnly) {
429  flags = O_RDWR | O_CREAT;
430  } else {
431  flags = O_RDONLY;
432  }
433  } else if (mode & QIODevice::WriteOnly) {
434  flags = O_WRONLY | O_CREAT;
435  }
436 
437  if (mode & QIODevice::Append) {
438  flags |= O_APPEND;
439  } else if (mode & QIODevice::Truncate) {
440  flags |= O_TRUNC;
441  }
442 
443  int fd = -1;
444  if ( flags & O_CREAT)
445  fd = KDE::open( openPath, flags, 0666);
446  else
447  fd = KDE::open( openPath, flags);
448  if ( fd < 0 ) {
449  error(KIO::ERR_CANNOT_OPEN_FOR_READING, openPath);
450  return;
451  }
452  // Determine the mimetype of the file to be retrieved, and emit it.
453  // This is mandatory in all slaves (for KRun/BrowserRun to work).
454  // If we're not opening the file ReadOnly or ReadWrite, don't attempt to
455  // read the file and send the mimetype.
456  if (mode & QIODevice::ReadOnly){
457  KMimeType::Ptr mt = KMimeType::findByUrl( url, buff.st_mode, true /* local URL */ );
458  emit mimeType( mt->name() );
459  }
460 
461  totalSize( buff.st_size );
462  position( 0 );
463 
464  emit opened();
465  openFd = fd;
466 }
467 
468 void FileProtocol::read(KIO::filesize_t bytes)
469 {
470  kDebug(7101) << "File::open -- read";
471  Q_ASSERT(openFd != -1);
472 
473  QVarLengthArray<char> buffer(bytes);
474  while (true) {
475  int res;
476  do {
477  res = ::read(openFd, buffer.data(), bytes);
478  } while (res == -1 && errno == EINTR);
479 
480  if (res > 0) {
481  QByteArray array = QByteArray::fromRawData(buffer.data(), res);
482  data( array );
483  bytes -= res;
484  } else {
485  // empty array designates eof
486  data(QByteArray());
487  if (res != 0) {
488  error(KIO::ERR_COULD_NOT_READ, openPath);
489  close();
490  }
491  break;
492  }
493  if (bytes <= 0) break;
494  }
495 }
496 
497 void FileProtocol::write(const QByteArray &data)
498 {
499  kDebug(7101) << "File::open -- write";
500  Q_ASSERT(openFd != -1);
501 
502  if (write_all(openFd, data.constData(), data.size())) {
503  if (errno == ENOSPC) { // disk full
504  error(KIO::ERR_DISK_FULL, openPath);
505  close();
506  } else {
507  kWarning(7101) << "Couldn't write. Error:" << strerror(errno);
508  error(KIO::ERR_COULD_NOT_WRITE, openPath);
509  close();
510  }
511  } else {
512  written(data.size());
513  }
514 }
515 
516 void FileProtocol::seek(KIO::filesize_t offset)
517 {
518  kDebug(7101) << "File::open -- seek";
519  Q_ASSERT(openFd != -1);
520 
521  int res = KDE_lseek(openFd, offset, SEEK_SET);
522  if (res != -1) {
523  position( offset );
524  } else {
525  error(KIO::ERR_COULD_NOT_SEEK, openPath);
526  close();
527  }
528 }
529 
530 void FileProtocol::close()
531 {
532  kDebug(7101) << "File::open -- close ";
533  Q_ASSERT(openFd != -1);
534 
535  ::close( openFd );
536  openFd = -1;
537  openPath.clear();
538 
539  finished();
540 }
541 
542 void FileProtocol::put( const KUrl& url, int _mode, KIO::JobFlags _flags )
543 {
544  const QString dest_orig = url.toLocalFile();
545 
546  kDebug(7101) << dest_orig << "mode=" << _mode;
547 
548  QString dest_part(dest_orig + QLatin1String(".part"));
549 
550  KDE_struct_stat buff_orig;
551  const bool bOrigExists = (KDE::lstat(dest_orig, &buff_orig) != -1);
552  bool bPartExists = false;
553  const bool bMarkPartial = config()->readEntry("MarkPartial", true);
554 
555  if (bMarkPartial)
556  {
557  KDE_struct_stat buff_part;
558  bPartExists = (KDE::stat( dest_part, &buff_part ) != -1);
559 
560  if (bPartExists && !(_flags & KIO::Resume) && !(_flags & KIO::Overwrite) && buff_part.st_size > 0 && S_ISREG(buff_part.st_mode))
561  {
562  kDebug(7101) << "calling canResume with" << KIO::number(buff_part.st_size);
563 
564  // Maybe we can use this partial file for resuming
565  // Tell about the size we have, and the app will tell us
566  // if it's ok to resume or not.
567  _flags |= canResume( buff_part.st_size ) ? KIO::Resume : KIO::DefaultFlags;
568 
569  kDebug(7101) << "got answer" << (_flags & KIO::Resume);
570  }
571  }
572 
573  if ( bOrigExists && !(_flags & KIO::Overwrite) && !(_flags & KIO::Resume))
574  {
575  if (S_ISDIR(buff_orig.st_mode))
576  error( KIO::ERR_DIR_ALREADY_EXIST, dest_orig );
577  else
578  error( KIO::ERR_FILE_ALREADY_EXIST, dest_orig );
579  return;
580  }
581 
582  int result;
583  QString dest;
584  QByteArray _dest;
585 
586  int fd = -1;
587 
588  // Loop until we got 0 (end of data)
589  do
590  {
591  QByteArray buffer;
592  dataReq(); // Request for data
593  result = readData( buffer );
594 
595  if (result >= 0)
596  {
597  if (dest.isEmpty())
598  {
599  if (bMarkPartial)
600  {
601  kDebug(7101) << "Appending .part extension to" << dest_orig;
602  dest = dest_part;
603  if ( bPartExists && !(_flags & KIO::Resume) )
604  {
605  kDebug(7101) << "Deleting partial file" << dest_part;
606  QFile::remove( dest_part );
607  // Catch errors when we try to open the file.
608  }
609  }
610  else
611  {
612  dest = dest_orig;
613  if ( bOrigExists && !(_flags & KIO::Resume) )
614  {
615  kDebug(7101) << "Deleting destination file" << dest_orig;
616  QFile::remove( dest_orig );
617  // Catch errors when we try to open the file.
618  }
619  }
620 
621  if ( (_flags & KIO::Resume) )
622  {
623  fd = KDE::open( dest, O_RDWR ); // append if resuming
624  KDE_lseek(fd, 0, SEEK_END); // Seek to end
625  }
626  else
627  {
628  // WABA: Make sure that we keep writing permissions ourselves,
629  // otherwise we can be in for a surprise on NFS.
630  mode_t initialMode;
631  if (_mode != -1)
632  initialMode = _mode | S_IWUSR | S_IRUSR;
633  else
634  initialMode = 0666;
635 
636  fd = KDE::open(dest, O_CREAT | O_TRUNC | O_WRONLY, initialMode);
637  }
638 
639  if ( fd < 0 )
640  {
641  kDebug(7101) << "####################### COULD NOT WRITE" << dest << "_mode=" << _mode;
642  kDebug(7101) << "errno==" << errno << "(" << strerror(errno) << ")";
643  if ( errno == EACCES )
644  error(KIO::ERR_WRITE_ACCESS_DENIED, dest);
645  else
646  error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest);
647  return;
648  }
649  }
650 
651  if (write_all( fd, buffer.data(), buffer.size()))
652  {
653  if ( errno == ENOSPC ) // disk full
654  {
655  error(KIO::ERR_DISK_FULL, dest_orig);
656  result = -2; // means: remove dest file
657  }
658  else
659  {
660  kWarning(7101) << "Couldn't write. Error:" << strerror(errno);
661  error(KIO::ERR_COULD_NOT_WRITE, dest_orig);
662  result = -1;
663  }
664  }
665  }
666  }
667  while ( result > 0 );
668 
669  // An error occurred deal with it.
670  if (result < 0)
671  {
672  kDebug(7101) << "Error during 'put'. Aborting.";
673 
674  if (fd != -1)
675  {
676  ::close(fd);
677 
678  KDE_struct_stat buff;
679  if (bMarkPartial && KDE::stat( dest, &buff ) == 0)
680  {
681  int size = config()->readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE);
682  if (buff.st_size < size)
683  remove(_dest.data());
684  }
685  }
686 
687  ::exit(255);
688  }
689 
690  if ( fd == -1 ) // we got nothing to write out, so we never opened the file
691  {
692  finished();
693  return;
694  }
695 
696  if ( ::close(fd) )
697  {
698  kWarning(7101) << "Error when closing file descriptor:" << strerror(errno);
699  error(KIO::ERR_COULD_NOT_WRITE, dest_orig);
700  return;
701  }
702 
703  // after full download rename the file back to original name
704  if ( bMarkPartial )
705  {
706  // If the original URL is a symlink and we were asked to overwrite it,
707  // remove the symlink first. This ensures that we do not overwrite the
708  // current source if the symlink points to it.
709  if( (_flags & KIO::Overwrite) && S_ISLNK( buff_orig.st_mode ) )
710  QFile::remove( dest_orig );
711  if ( KDE::rename( dest, dest_orig ) )
712  {
713  kWarning(7101) << " Couldn't rename " << _dest << " to " << dest_orig;
714  error(KIO::ERR_CANNOT_RENAME_PARTIAL, dest_orig);
715  return;
716  }
717  org::kde::KDirNotify::emitFileRenamed(dest, dest_orig);
718  }
719 
720  // set final permissions
721  if ( _mode != -1 && !(_flags & KIO::Resume) )
722  {
723  if (KDE::chmod(dest_orig, _mode) != 0)
724  {
725  // couldn't chmod. Eat the error if the filesystem apparently doesn't support it.
726  KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(dest_orig);
727  if (mp && mp->testFileSystemFlag(KMountPoint::SupportsChmod))
728  warning( i18n( "Could not change permissions for\n%1" , dest_orig ) );
729  }
730  }
731 
732  // set modification time
733  const QString mtimeStr = metaData(QLatin1String("modified"));
734  if ( !mtimeStr.isEmpty() ) {
735  QDateTime dt = QDateTime::fromString( mtimeStr, Qt::ISODate );
736  if ( dt.isValid() ) {
737  KDE_struct_stat dest_statbuf;
738  if (KDE::stat( dest_orig, &dest_statbuf ) == 0) {
739  struct timeval utbuf[2];
740  // access time
741  utbuf[0].tv_sec = dest_statbuf.st_atime; // access time, unchanged ## TODO preserve msec
742  utbuf[0].tv_usec = 0;
743  // modification time
744  utbuf[1].tv_sec = dt.toTime_t();
745  utbuf[1].tv_usec = dt.time().msec() * 1000;
746  utimes( QFile::encodeName(dest_orig), utbuf );
747  }
748  }
749 
750  }
751 
752  // We have done our job => finish
753  finished();
754 }
755 
756 QString FileProtocol::getUserName( uid_t uid ) const
757 {
758  if ( !mUsercache.contains( uid ) ) {
759  struct passwd *user = getpwuid( uid );
760  if ( user ) {
761  mUsercache.insert( uid, QString::fromLatin1(user->pw_name) );
762  }
763  else
764  return QString::number( uid );
765  }
766  return mUsercache[uid];
767 }
768 
769 QString FileProtocol::getGroupName( gid_t gid ) const
770 {
771  if ( !mGroupcache.contains( gid ) ) {
772  struct group *grp = getgrgid( gid );
773  if ( grp ) {
774  mGroupcache.insert( gid, QString::fromLatin1(grp->gr_name) );
775  }
776  else
777  return QString::number( gid );
778  }
779  return mGroupcache[gid];
780 }
781 
782 bool FileProtocol::createUDSEntry( const QString & filename, const QByteArray & path, UDSEntry & entry,
783  short int details, bool withACL )
784 {
785 #ifndef HAVE_POSIX_ACL
786  Q_UNUSED(withACL);
787 #endif
788  assert(entry.count() == 0); // by contract :-)
789  // entry.reserve( 8 ); // speed up QHash insertion
790 
791  entry.insert( KIO::UDSEntry::UDS_NAME, filename );
792 
793  mode_t type;
794  mode_t access;
795  KDE_struct_stat buff;
796 
797  if ( KDE_lstat( path.data(), &buff ) == 0 ) {
798 
799  if (details > 2) {
800  entry.insert( KIO::UDSEntry::UDS_DEVICE_ID, buff.st_dev );
801  entry.insert( KIO::UDSEntry::UDS_INODE, buff.st_ino );
802  }
803 
804  if (S_ISLNK(buff.st_mode)) {
805 
806  char buffer2[ 1000 ];
807  int n = readlink( path.data(), buffer2, 999 );
808  if ( n != -1 ) {
809  buffer2[ n ] = 0;
810  }
811 
812  entry.insert( KIO::UDSEntry::UDS_LINK_DEST, QFile::decodeName( buffer2 ) );
813 
814  // A symlink -> follow it only if details>1
815  if ( details > 1 && KDE_stat( path.data(), &buff ) == -1 ) {
816  // It is a link pointing to nowhere
817  type = S_IFMT - 1;
818  access = S_IRWXU | S_IRWXG | S_IRWXO;
819 
820  entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, type );
821  entry.insert( KIO::UDSEntry::UDS_ACCESS, access );
822  entry.insert( KIO::UDSEntry::UDS_SIZE, 0LL );
823  goto notype;
824 
825  }
826  }
827  } else {
828  // kWarning() << "lstat didn't work on " << path.data();
829  return false;
830  }
831 
832  type = buff.st_mode & S_IFMT; // extract file type
833  access = buff.st_mode & 07777; // extract permissions
834 
835  entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, type );
836  entry.insert( KIO::UDSEntry::UDS_ACCESS, access );
837 
838  entry.insert( KIO::UDSEntry::UDS_SIZE, buff.st_size );
839 
840 #ifdef HAVE_POSIX_ACL
841  if (details > 0) {
842  /* Append an atom indicating whether the file has extended acl information
843  * and if withACL is specified also one with the acl itself. If it's a directory
844  * and it has a default ACL, also append that. */
845  appendACLAtoms( path, entry, type, withACL );
846  }
847 #endif
848 
849  notype:
850  if (details > 0) {
851  entry.insert( KIO::UDSEntry::UDS_MODIFICATION_TIME, buff.st_mtime );
852  entry.insert( KIO::UDSEntry::UDS_USER, getUserName( buff.st_uid ) );
853  entry.insert( KIO::UDSEntry::UDS_GROUP, getGroupName( buff.st_gid ) );
854  entry.insert( KIO::UDSEntry::UDS_ACCESS_TIME, buff.st_atime );
855  }
856 
857  // Note: buff.st_ctime isn't the creation time !
858  // We made that mistake for KDE 2.0, but it's in fact the
859  // "file status" change time, which we don't care about.
860 
861  return true;
862 }
863 
864 void FileProtocol::special( const QByteArray &data)
865 {
866  int tmp;
867  QDataStream stream(data);
868 
869  stream >> tmp;
870  switch (tmp) {
871  case 1:
872  {
873  QString fstype, dev, point;
874  qint8 iRo;
875 
876  stream >> iRo >> fstype >> dev >> point;
877 
878  bool ro = ( iRo != 0 );
879 
880  kDebug(7101) << "MOUNTING fstype=" << fstype << " dev=" << dev << " point=" << point << " ro=" << ro;
881  bool ok = pmount( dev );
882  if (ok)
883  finished();
884  else
885  mount( ro, fstype.toAscii(), dev, point );
886 
887  }
888  break;
889  case 2:
890  {
891  QString point;
892  stream >> point;
893  bool ok = pumount( point );
894  if (ok)
895  finished();
896  else
897  unmount( point );
898  }
899  break;
900 
901  default:
902  break;
903  }
904 }
905 
906 void FileProtocol::mount( bool _ro, const char *_fstype, const QString& _dev, const QString& _point )
907 {
908  kDebug(7101) << "fstype=" << _fstype;
909 
910 #ifndef _WIN32_WCE
911 #ifdef HAVE_VOLMGT
912  /*
913  * support for Solaris volume management
914  */
915  QString err;
916  QByteArray devname = QFile::encodeName( _dev );
917 
918  if( volmgt_running() ) {
919 // kDebug(7101) << "VOLMGT: vold ok.";
920  if( volmgt_check( devname.data() ) == 0 ) {
921  kDebug(7101) << "VOLMGT: no media in "
922  << devname.data();
923  err = i18n("No Media inserted or Media not recognized.");
924  error( KIO::ERR_COULD_NOT_MOUNT, err );
925  return;
926  } else {
927  kDebug(7101) << "VOLMGT: " << devname.data()
928  << ": media ok";
929  finished();
930  return;
931  }
932  } else {
933  err = i18n("\"vold\" is not running.");
934  kDebug(7101) << "VOLMGT: " << err;
935  error( KIO::ERR_COULD_NOT_MOUNT, err );
936  return;
937  }
938 #else
939 
940 
941  KTemporaryFile tmpFile;
942  tmpFile.setAutoRemove(false);
943  tmpFile.open();
944  QByteArray tmpFileName = QFile::encodeName(tmpFile.fileName());
945  QByteArray dev;
946  if (_dev.startsWith(QLatin1String("LABEL="))) { // turn LABEL=foo into -L foo (#71430)
947  QString labelName = _dev.mid( 6 );
948  dev = "-L ";
949  dev += QFile::encodeName( KShell::quoteArg( labelName ) ); // is it correct to assume same encoding as filesystem?
950  } else if (_dev.startsWith(QLatin1String("UUID="))) { // and UUID=bar into -U bar
951  QString uuidName = _dev.mid( 5 );
952  dev = "-U ";
953  dev += QFile::encodeName( KShell::quoteArg( uuidName ) );
954  }
955  else
956  dev = QFile::encodeName( KShell::quoteArg(_dev) ); // get those ready to be given to a shell
957 
958  QByteArray point = QFile::encodeName( KShell::quoteArg(_point) );
959  bool fstype_empty = !_fstype || !*_fstype;
960  QByteArray fstype = KShell::quoteArg(QString::fromLatin1(_fstype)).toLatin1(); // good guess
961  QByteArray readonly = _ro ? "-r" : "";
962  QString epath = QString::fromLocal8Bit(qgetenv("PATH"));
963  QString path = QLatin1String("/sbin:/bin");
964  if(!epath.isEmpty())
965  path += QLatin1String(":") + epath;
966  QByteArray mountProg = KGlobal::dirs()->findExe(QLatin1String("mount"), path).toLocal8Bit();
967  if (mountProg.isEmpty()){
968  error( KIO::ERR_COULD_NOT_MOUNT, i18n("Could not find program \"mount\""));
969  return;
970  }
971 
972  // Two steps, in case mount doesn't like it when we pass all options
973  for ( int step = 0 ; step <= 1 ; step++ )
974  {
975  QByteArray buffer = mountProg + ' ';
976  // Mount using device only if no fstype nor mountpoint (KDE-1.x like)
977  if ( !dev.isEmpty() && _point.isEmpty() && fstype_empty )
978  buffer += dev;
979  else
980  // Mount using the mountpoint, if no fstype nor device (impossible in first step)
981  if ( !_point.isEmpty() && dev.isEmpty() && fstype_empty )
982  buffer += point;
983  else
984  // mount giving device + mountpoint but no fstype
985  if ( !_point.isEmpty() && !dev.isEmpty() && fstype_empty )
986  buffer += readonly + ' ' + dev + ' ' + point;
987  else
988  // mount giving device + mountpoint + fstype
989 #if defined(__svr4__) && defined(Q_OS_SOLARIS) // MARCO for Solaris 8 and I
990  // believe this is true for SVR4 in general
991  buffer += "-F " + fstype + ' ' + (_ro ? "-oro" : "") + ' ' + dev + ' ' + point;
992 #else
993  buffer += readonly + " -t " + fstype + ' ' + dev + ' ' + point;
994 #endif
995  buffer += " 2>" + tmpFileName;
996  kDebug(7101) << buffer;
997 
998  int mount_ret = system( buffer.constData() );
999 
1000  QString err = testLogFile( tmpFileName );
1001  if ( err.isEmpty() && mount_ret == 0)
1002  {
1003  finished();
1004  return;
1005  }
1006  else
1007  {
1008  // Didn't work - or maybe we just got a warning
1009  KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice( _dev );
1010  // Is the device mounted ?
1011  if ( mp && mount_ret == 0)
1012  {
1013  kDebug(7101) << "mount got a warning:" << err;
1014  warning( err );
1015  finished();
1016  return;
1017  }
1018  else
1019  {
1020  if ( (step == 0) && !_point.isEmpty())
1021  {
1022  kDebug(7101) << err;
1023  kDebug(7101) << "Mounting with those options didn't work, trying with only mountpoint";
1024  fstype = "";
1025  fstype_empty = true;
1026  dev = "";
1027  // The reason for trying with only mountpoint (instead of
1028  // only device) is that some people (hi Malte!) have the
1029  // same device associated with two mountpoints
1030  // for different fstypes, like /dev/fd0 /mnt/e2floppy and
1031  // /dev/fd0 /mnt/dosfloppy.
1032  // If the user has the same mountpoint associated with two
1033  // different devices, well they shouldn't specify the
1034  // mountpoint but just the device.
1035  }
1036  else
1037  {
1038  error( KIO::ERR_COULD_NOT_MOUNT, err );
1039  return;
1040  }
1041  }
1042  }
1043  }
1044 #endif /* ! HAVE_VOLMGT */
1045 #else
1046  QString err;
1047  err = i18n("mounting is not supported by wince.");
1048  error( KIO::ERR_COULD_NOT_MOUNT, err );
1049 #endif
1050 
1051 }
1052 
1053 
1054 void FileProtocol::unmount( const QString& _point )
1055 {
1056 #ifndef _WIN32_WCE
1057  QByteArray buffer;
1058 
1059  KTemporaryFile tmpFile;
1060  tmpFile.setAutoRemove(false);
1061  tmpFile.open();
1062  QByteArray tmpFileName = QFile::encodeName(tmpFile.fileName());
1063  QString err;
1064 
1065 #ifdef HAVE_VOLMGT
1066  /*
1067  * support for Solaris volume management
1068  */
1069  char *devname;
1070  char *ptr;
1071  FILE *mnttab;
1072  struct mnttab mnt;
1073 
1074  if( volmgt_running() ) {
1075  kDebug(7101) << "VOLMGT: looking for "
1076  << _point.toLocal8Bit();
1077 
1078  if( (mnttab = KDE_fopen( MNTTAB, "r" )) == NULL ) {
1079  err = QLatin1String("could not open mnttab");
1080  kDebug(7101) << "VOLMGT: " << err;
1081  error( KIO::ERR_COULD_NOT_UNMOUNT, err );
1082  return;
1083  }
1084 
1085  /*
1086  * since there's no way to derive the device name from
1087  * the mount point through the volmgt library (and
1088  * media_findname() won't work in this case), we have to
1089  * look ourselves...
1090  */
1091  devname = NULL;
1092  rewind( mnttab );
1093  while( getmntent( mnttab, &mnt ) == 0 ) {
1094  if( strcmp( _point.toLocal8Bit(), mnt.mnt_mountp ) == 0 ){
1095  devname = mnt.mnt_special;
1096  break;
1097  }
1098  }
1099  fclose( mnttab );
1100 
1101  if( devname == NULL ) {
1102  err = QLatin1String("not in mnttab");
1103  kDebug(7101) << "VOLMGT: "
1104  << QFile::encodeName(_point).data()
1105  << ": " << err;
1106  error( KIO::ERR_COULD_NOT_UNMOUNT, err );
1107  return;
1108  }
1109 
1110  /*
1111  * strip off the directory name (volume name)
1112  * the eject(1) command will handle unmounting and
1113  * physically eject the media (if possible)
1114  */
1115  ptr = strrchr( devname, '/' );
1116  *ptr = '\0';
1117  QByteArray qdevname(QFile::encodeName(KShell::quoteArg(QFile::decodeName(QByteArray(devname)))).data());
1118  buffer = "/usr/bin/eject " + qdevname + " 2>" + tmpFileName;
1119  kDebug(7101) << "VOLMGT: eject " << qdevname;
1120 
1121  /*
1122  * from eject(1): exit status == 0 => need to manually eject
1123  * exit status == 4 => media was ejected
1124  */
1125  if( WEXITSTATUS( system( buffer.constData() )) == 4 ) {
1126  /*
1127  * this is not an error, so skip "testLogFile()"
1128  * to avoid wrong/confusing error popup. The
1129  * temporary file is removed by KTemporaryFile's
1130  * destructor, so don't do that manually.
1131  */
1132  finished();
1133  return;
1134  }
1135  } else {
1136  /*
1137  * eject(1) should do its job without vold(1M) running,
1138  * so we probably could call eject anyway, but since the
1139  * media is mounted now, vold must've died for some reason
1140  * during the user's session, so it should be restarted...
1141  */
1142  err = i18n("\"vold\" is not running.");
1143  kDebug(7101) << "VOLMGT: " << err;
1144  error( KIO::ERR_COULD_NOT_UNMOUNT, err );
1145  return;
1146  }
1147 #else
1148  QString epath = QString::fromLocal8Bit(qgetenv("PATH"));
1149  QString path = QLatin1String("/sbin:/bin");
1150  if (!epath.isEmpty())
1151  path += QLatin1Char(':') + epath;
1152  QByteArray umountProg = KGlobal::dirs()->findExe(QLatin1String("umount"), path).toLocal8Bit();
1153 
1154  if (umountProg.isEmpty()) {
1155  error( KIO::ERR_COULD_NOT_UNMOUNT, i18n("Could not find program \"umount\""));
1156  return;
1157  }
1158  buffer = umountProg + ' ' + QFile::encodeName(KShell::quoteArg(_point)) + " 2>" + tmpFileName;
1159  system( buffer.constData() );
1160 #endif /* HAVE_VOLMGT */
1161 
1162  err = testLogFile( tmpFileName );
1163  if ( err.isEmpty() )
1164  finished();
1165  else
1166  error( KIO::ERR_COULD_NOT_UNMOUNT, err );
1167 #else
1168  QString err;
1169  err = i18n("unmounting is not supported by wince.");
1170  error( KIO::ERR_COULD_NOT_MOUNT, err );
1171 #endif
1172 }
1173 
1174 /*************************************
1175  *
1176  * pmount handling
1177  *
1178  *************************************/
1179 
1180 bool FileProtocol::pmount(const QString &dev)
1181 {
1182 #ifndef _WIN32_WCE
1183  QString epath = QString::fromLocal8Bit(qgetenv("PATH"));
1184  QString path = QLatin1String("/sbin:/bin");
1185  if (!epath.isEmpty())
1186  path += QLatin1Char(':') + epath;
1187  QString pmountProg = KGlobal::dirs()->findExe(QLatin1String("pmount"), path);
1188 
1189  if (pmountProg.isEmpty())
1190  return false;
1191 
1192  QByteArray buffer = QFile::encodeName(pmountProg) + ' ' +
1193  QFile::encodeName(KShell::quoteArg(dev));
1194 
1195  int res = system( buffer.constData() );
1196 
1197  return res==0;
1198 #else
1199  return false;
1200 #endif
1201 }
1202 
1203 bool FileProtocol::pumount(const QString &point)
1204 {
1205 #ifndef _WIN32_WCE
1206  KMountPoint::Ptr mp = KMountPoint::currentMountPoints(KMountPoint::NeedRealDeviceName).findByPath(point);
1207  if (!mp)
1208  return false;
1209  QString dev = mp->realDeviceName();
1210  if (dev.isEmpty()) return false;
1211 
1212  QString epath = QString::fromLocal8Bit(qgetenv("PATH"));
1213  QString path = QLatin1String("/sbin:/bin");
1214  if (!epath.isEmpty())
1215  path += QLatin1Char(':') + epath;
1216  QString pumountProg = KGlobal::dirs()->findExe(QLatin1String("pumount"), path);
1217 
1218  if (pumountProg.isEmpty())
1219  return false;
1220 
1221  QByteArray buffer = QFile::encodeName(pumountProg);
1222  buffer += ' ';
1223  buffer += QFile::encodeName(KShell::quoteArg(dev));
1224 
1225  int res = system( buffer.data() );
1226 
1227  return res==0;
1228 #else
1229  return false;
1230 #endif
1231 }
1232 
1233 /*************************************
1234  *
1235  * Utilities
1236  *
1237  *************************************/
1238 
1239 static QString testLogFile( const QByteArray& _filename )
1240 {
1241  char buffer[ 1024 ];
1242  KDE_struct_stat buff;
1243 
1244  QString result;
1245 
1246  KDE_stat( _filename, &buff );
1247  int size = buff.st_size;
1248  if ( size == 0 ) {
1249  unlink( _filename );
1250  return result;
1251  }
1252 
1253  FILE * f = KDE_fopen( _filename, "rb" );
1254  if ( f == 0L ) {
1255  unlink( _filename );
1256  result = i18n("Could not read %1", QFile::decodeName(_filename));
1257  return result;
1258  }
1259 
1260  result.clear();
1261  const char *p = "";
1262  while ( p != 0L ) {
1263  p = fgets( buffer, sizeof(buffer)-1, f );
1264  if ( p != 0L )
1265  result += QString::fromLocal8Bit(buffer);
1266  }
1267 
1268  fclose( f );
1269 
1270  unlink( _filename );
1271 
1272  return result;
1273 }
1274 
1275 /*************************************
1276  *
1277  * ACL handling helpers
1278  *
1279  *************************************/
1280 #ifdef HAVE_POSIX_ACL
1281 
1282 static bool isExtendedACL( acl_t acl )
1283 {
1284  return ( acl_equiv_mode( acl, 0 ) != 0 );
1285 }
1286 
1287 static void appendACLAtoms( const QByteArray & path, UDSEntry& entry, mode_t type, bool withACL )
1288 {
1289  // first check for a noop
1290  if ( acl_extended_file( path.data() ) == 0 ) return;
1291 
1292  acl_t acl = 0;
1293  acl_t defaultAcl = 0;
1294  bool isDir = S_ISDIR( type );
1295  // do we have an acl for the file, and/or a default acl for the dir, if it is one?
1296  acl = acl_get_file( path.data(), ACL_TYPE_ACCESS );
1297  /* Sadly libacl does not provided a means of checking for extended ACL and default
1298  * ACL separately. Since a directory can have both, we need to check again. */
1299  if ( isDir ) {
1300  if ( acl ) {
1301  if ( !isExtendedACL( acl ) ) {
1302  acl_free( acl );
1303  acl = 0;
1304  }
1305  }
1306  defaultAcl = acl_get_file( path.data(), ACL_TYPE_DEFAULT );
1307  }
1308  if ( acl || defaultAcl ) {
1309  kDebug(7101) << path.constData() << "has extended ACL entries";
1310  entry.insert( KIO::UDSEntry::UDS_EXTENDED_ACL, 1 );
1311  }
1312  if ( withACL ) {
1313  if ( acl ) {
1314  const QString str = aclToText(acl);
1315  entry.insert( KIO::UDSEntry::UDS_ACL_STRING, str );
1316  kDebug(7101) << path.constData() << "ACL:" << str;
1317  }
1318  if ( defaultAcl ) {
1319  const QString str = aclToText(defaultAcl);
1320  entry.insert( KIO::UDSEntry::UDS_DEFAULT_ACL_STRING, str );
1321  kDebug(7101) << path.constData() << "DEFAULT ACL:" << str;
1322  }
1323  }
1324  if ( acl ) acl_free( acl );
1325  if ( defaultAcl ) acl_free( defaultAcl );
1326 }
1327 #endif
1328 
1329 // We could port this to KTempDir::removeDir but then we wouldn't be able to tell the user
1330 // where exactly the deletion failed, in case of errors.
1331 bool FileProtocol::deleteRecursive(const QString& path)
1332 {
1333  //kDebug() << path;
1334  QDirIterator it(path, QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden,
1335  QDirIterator::Subdirectories);
1336  QStringList dirsToDelete;
1337  while ( it.hasNext() ) {
1338  const QString itemPath = it.next();
1339  //kDebug() << "itemPath=" << itemPath;
1340  const QFileInfo info = it.fileInfo();
1341  if (info.isDir() && !info.isSymLink())
1342  dirsToDelete.prepend(itemPath);
1343  else {
1344  //kDebug() << "QFile::remove" << itemPath;
1345  if (!QFile::remove(itemPath)) {
1346  error(KIO::ERR_CANNOT_DELETE, itemPath);
1347  return false;
1348  }
1349  }
1350  }
1351  QDir dir;
1352  Q_FOREACH(const QString& itemPath, dirsToDelete) {
1353  //kDebug() << "QDir::rmdir" << itemPath;
1354  if (!dir.rmdir(itemPath)) {
1355  error(KIO::ERR_CANNOT_DELETE, itemPath);
1356  return false;
1357  }
1358  }
1359  return true;
1360 }
1361 
1362 #include "file.moc"
This file is part of the KDE documentation.
Documentation copyright © 1996-2012 The KDE developers.
Generated on Fri Dec 7 2012 16:13:29 by doxygen 1.8.1 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

KIOSlave

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

kdelibs-4.8.5 API Reference

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