• Skip to content
  • Skip to link menu
  • KDE API Reference
  • kdepimlibs-4.10.5 API Reference
  • KDE Home
  • Contact Us
 

akonadi

  • akonadi
  • calendar
incidencechanger.cpp
1 /*
2  Copyright (C) 2004 Reinhold Kainhofer <reinhold@kainhofer.com>
3  Copyright (C) 2010-2012 Sérgio Martins <iamsergio@gmail.com>
4 
5  This library is free software; you can redistribute it and/or modify it
6  under the terms of the GNU Library General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or (at your
8  option) any later version.
9 
10  This library is distributed in the hope that it will be useful, but WITHOUT
11  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13  License for more details.
14 
15  You should have received a copy of the GNU Library General Public License
16  along with this library; see the file COPYING.LIB. If not, write to the
17  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18  02110-1301, USA.
19 */
20 #include "incidencechanger.h"
21 #include "incidencechanger_p.h"
22 #include "mailscheduler_p.h"
23 #include "utils_p.h"
24 
25 #include <akonadi/itemcreatejob.h>
26 #include <akonadi/itemmodifyjob.h>
27 #include <akonadi/itemdeletejob.h>
28 #include <akonadi/transactionsequence.h>
29 #include <akonadi/collectiondialog.h>
30 
31 #include <KJob>
32 #include <KLocale>
33 #include <KGuiItem>
34 #include <KMessageBox>
35 #include <KStandardGuiItem>
36 
37 using namespace Akonadi;
38 using namespace KCalCore;
39 
40 InvitationHandlerHelper::Action actionFromStatus( InvitationHandlerHelper::SendResult result )
41 {
42  //enum SendResult {
43  // Canceled, /**< Sending was canceled by the user, meaning there are
44  // local changes of which other attendees are not aware. */
45  // FailKeepUpdate, /**< Sending failed, the changes to the incidence must be kept. */
46  // FailAbortUpdate, /**< Sending failed, the changes to the incidence must be undone. */
47  // NoSendingNeeded, /**< In some cases it is not needed to send an invitation
48  // (e.g. when we are the only attendee) */
49  // Success
50  switch ( result ) {
51  case InvitationHandlerHelper::ResultCanceled:
52  return InvitationHandlerHelper::ActionDontSendMessage;
53  case InvitationHandlerHelper::ResultSuccess:
54  return InvitationHandlerHelper::ActionSendMessage;
55  default:
56  return InvitationHandlerHelper::ActionAsk;
57  }
58 }
59 
60 namespace Akonadi {
61  static Akonadi::Collection selectCollection( QWidget *parent,
62  int &dialogCode,
63  const QStringList &mimeTypes,
64  const Akonadi::Collection &defCollection )
65  {
66  QPointer<Akonadi::CollectionDialog> dlg( new Akonadi::CollectionDialog( parent ) );
67 
68  kDebug() << "selecting collections with mimeType in " << mimeTypes;
69 
70  dlg->setMimeTypeFilter( mimeTypes );
71  dlg->setAccessRightsFilter( Akonadi::Collection::CanCreateItem );
72  if ( defCollection.isValid() ) {
73  dlg->setDefaultCollection( defCollection );
74  }
75  Akonadi::Collection collection;
76 
77  // FIXME: don't use exec.
78  dialogCode = dlg->exec();
79  if ( dialogCode == QDialog::Accepted ) {
80  collection = dlg->selectedCollection();
81 
82  if ( !collection.isValid() ) {
83  kWarning() <<"An invalid collection was selected!";
84  }
85  }
86  delete dlg;
87 
88  return collection;
89  }
90 
91  // Does a queued emit, with QMetaObject::invokeMethod
92  static void emitCreateFinished( IncidenceChanger *changer,
93  int changeId,
94  const Akonadi::Item &item,
95  Akonadi::IncidenceChanger::ResultCode resultCode,
96  const QString &errorString )
97  {
98  QMetaObject::invokeMethod( changer, "createFinished", Qt::QueuedConnection,
99  Q_ARG( int, changeId ),
100  Q_ARG( Akonadi::Item, item ),
101  Q_ARG( Akonadi::IncidenceChanger::ResultCode, resultCode ),
102  Q_ARG( QString, errorString ) );
103  }
104 
105  // Does a queued emit, with QMetaObject::invokeMethod
106  static void emitModifyFinished( IncidenceChanger *changer,
107  int changeId,
108  const Akonadi::Item &item,
109  IncidenceChanger::ResultCode resultCode,
110  const QString &errorString )
111  {
112  QMetaObject::invokeMethod( changer, "modifyFinished", Qt::QueuedConnection,
113  Q_ARG( int, changeId ),
114  Q_ARG( Akonadi::Item, item ),
115  Q_ARG( Akonadi::IncidenceChanger::ResultCode, resultCode ),
116  Q_ARG( QString, errorString ) );
117  }
118 
119  // Does a queued emit, with QMetaObject::invokeMethod
120  static void emitDeleteFinished( IncidenceChanger *changer,
121  int changeId,
122  const QVector<Akonadi::Item::Id> &itemIdList,
123  IncidenceChanger::ResultCode resultCode,
124  const QString &errorString )
125  {
126  QMetaObject::invokeMethod( changer, "deleteFinished", Qt::QueuedConnection,
127  Q_ARG( int, changeId ),
128  Q_ARG( QVector<Akonadi::Item::Id>, itemIdList ),
129  Q_ARG( Akonadi::IncidenceChanger::ResultCode, resultCode ),
130  Q_ARG( QString, errorString ) );
131  }
132 }
133 
134 class ConflictPreventerPrivate;
135 class ConflictPreventer {
136  friend class ConflictPreventerPrivate;
137 public:
138  static ConflictPreventer* self();
139 
140  // To avoid conflicts when the two modifications came from within the same application
141  QHash<Akonadi::Item::Id, int> mLatestRevisionByItemId;
142 private:
143  ConflictPreventer() {}
144  ~ConflictPreventer() {}
145 };
146 
147 class ConflictPreventerPrivate {
148 public:
149  ConflictPreventer instance;
150 };
151 
152 K_GLOBAL_STATIC( ConflictPreventerPrivate, sConflictPreventerPrivate );
153 
154 ConflictPreventer* ConflictPreventer::self()
155 {
156  return &sConflictPreventerPrivate->instance;
157 }
158 
159 IncidenceChanger::Private::Private( bool enableHistory, IncidenceChanger *qq ) : q( qq )
160 {
161  mLatestChangeId = 0;
162  mShowDialogsOnError = true;
163  mHistory = enableHistory ? new History( this ) : 0;
164  mUseHistory = enableHistory;
165  mDestinationPolicy = DestinationPolicyDefault;
166  mRespectsCollectionRights = false;
167  mGroupwareCommunication = false;
168  mLatestAtomicOperationId = 0;
169  mBatchOperationInProgress = false;
170 
171  qRegisterMetaType<QVector<Akonadi::Item::Id> >( "QVector<Akonadi::Item::Id>" );
172  qRegisterMetaType<Akonadi::Item::Id>( "Akonadi::Item::Id" );
173 
174  qRegisterMetaType<Akonadi::IncidenceChanger::ResultCode>(
175  "Akonadi::IncidenceChanger::ResultCode" );
176 }
177 
178 IncidenceChanger::Private::~Private()
179 {
180  if ( !mAtomicOperations.isEmpty() ||
181  !mQueuedModifications.isEmpty() ||
182  !mModificationsInProgress.isEmpty() ) {
183  kDebug() << "Normal if the application was being used. "
184  "But might indicate a memory leak if it wasn't";
185  }
186 }
187 
188 bool IncidenceChanger::Private::atomicOperationIsValid( uint atomicOperationId ) const
189 {
190  // Changes must be done between startAtomicOperation() and endAtomicOperation()
191  return mAtomicOperations.contains( atomicOperationId ) &&
192  !mAtomicOperations[atomicOperationId]->endCalled;
193 }
194 
195 bool IncidenceChanger::Private::hasRights( const Collection &collection,
196  IncidenceChanger::ChangeType changeType ) const
197 {
198  bool result = false;
199  switch( changeType ) {
200  case ChangeTypeCreate:
201  result = collection.rights() & Akonadi::Collection::CanCreateItem;
202  break;
203  case ChangeTypeModify:
204  result = collection.rights() & Akonadi::Collection::CanChangeItem;
205  break;
206  case ChangeTypeDelete:
207  result = collection.rights() & Akonadi::Collection::CanDeleteItem;
208  break;
209  default:
210  Q_ASSERT_X( false, "hasRights", "invalid type" );
211  }
212 
213  return !collection.isValid() || !mRespectsCollectionRights || result;
214 }
215 
216 Akonadi::Job* IncidenceChanger::Private::parentJob( const Change::Ptr &change ) const
217 {
218  return (mBatchOperationInProgress && !change->queuedModification) ?
219  mAtomicOperations[mLatestAtomicOperationId]->transaction : 0;
220 }
221 
222 void IncidenceChanger::Private::queueModification( Change::Ptr change )
223 {
224  // If there's already a change queued we just discard it
225  // and send the newer change, which already includes
226  // previous modifications
227  const Akonadi::Item::Id id = change->newItem.id();
228  if ( mQueuedModifications.contains( id ) ) {
229  Change::Ptr toBeDiscarded = mQueuedModifications.take( id );
230  toBeDiscarded->resultCode = ResultCodeModificationDiscarded;
231  toBeDiscarded->completed = true;
232  mChangeById.remove( toBeDiscarded->id );
233  }
234 
235  change->queuedModification = true;
236  mQueuedModifications[id] = change;
237 }
238 
239 void IncidenceChanger::Private::performNextModification( Akonadi::Item::Id id )
240 {
241  mModificationsInProgress.remove( id );
242 
243  if ( mQueuedModifications.contains( id ) ) {
244  const Change::Ptr change = mQueuedModifications.take( id );
245  performModification( change );
246  }
247 }
248 
249 void IncidenceChanger::Private::handleTransactionJobResult( KJob *job )
250 {
251  //kDebug();
252  TransactionSequence *transaction = qobject_cast<TransactionSequence*>( job );
253  Q_ASSERT( transaction );
254  Q_ASSERT( mAtomicOperationByTransaction.contains( transaction ) );
255 
256  const uint atomicOperationId = mAtomicOperationByTransaction.take( transaction );
257 
258  Q_ASSERT( mAtomicOperations.contains(atomicOperationId) );
259  AtomicOperation *operation = mAtomicOperations[atomicOperationId];
260  Q_ASSERT( operation );
261  Q_ASSERT( operation->id == atomicOperationId );
262  if ( job->error() ) {
263  if ( !operation->rolledback() )
264  operation->setRolledback();
265  kError() << "Transaction failed, everything was rolledback. "
266  << job->errorString();
267  } else {
268  Q_ASSERT( operation->endCalled );
269  Q_ASSERT( !operation->pendingJobs() );
270  }
271 
272  if ( !operation->pendingJobs() && operation->endCalled ) {
273  delete mAtomicOperations.take( atomicOperationId );
274  mBatchOperationInProgress = false;
275  } else {
276  operation->transactionCompleted = true;
277  }
278 }
279 
280 void IncidenceChanger::Private::handleCreateJobResult( KJob *job )
281 {
282  //kDebug();
283  QString errorString;
284  ResultCode resultCode = ResultCodeSuccess;
285 
286  Change::Ptr change = mChangeForJob.take( job );
287  mChangeById.remove( change->id );
288 
289  const ItemCreateJob *j = qobject_cast<const ItemCreateJob*>( job );
290  Q_ASSERT( j );
291  Akonadi::Item item = j->item();
292 
293  QString description;
294  if ( change->atomicOperationId != 0 ) {
295  AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
296  a->numCompletedChanges++;
297  change->completed = true;
298  description = a->description;
299  }
300 
301  if ( j->error() ) {
302  item = change->newItem;
303  resultCode = ResultCodeJobError;
304  errorString = j->errorString();
305  kError() << errorString;
306  if ( mShowDialogsOnError ) {
307  KMessageBox::sorry( change->parentWidget,
308  i18n( "Error while trying to create calendar item. Error was: %1",
309  errorString ) );
310  }
311  } else {
312  Q_ASSERT( item.isValid() );
313  Q_ASSERT( item.hasPayload<KCalCore::Incidence::Ptr>() );
314  change->newItem = item;
315  handleInvitationsAfterChange( change );
316  // for user undo/redo
317  if ( change->recordToHistory ) {
318  mHistory->recordCreation( item, description, change->atomicOperationId );
319  }
320  }
321 
322  change->errorString = errorString;
323  change->resultCode = resultCode;
324  // puff, change finally goes out of scope, and emits the incidenceCreated signal.
325 }
326 
327 void IncidenceChanger::Private::handleDeleteJobResult( KJob *job )
328 {
329  //kDebug();
330  QString errorString;
331  ResultCode resultCode = ResultCodeSuccess;
332 
333  Change::Ptr change = mChangeForJob.take( job );
334  mChangeById.remove( change->id );
335 
336  const ItemDeleteJob *j = qobject_cast<const ItemDeleteJob*>( job );
337  const Item::List items = j->deletedItems();
338 
339  QSharedPointer<DeletionChange> deletionChange = change.staticCast<DeletionChange>();
340 
341  foreach( const Akonadi::Item &item, items ) {
342  deletionChange->mItemIds.append( item.id() );
343  }
344  QString description;
345  if ( change->atomicOperationId != 0 ) {
346  AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
347  a->numCompletedChanges++;
348  change->completed = true;
349  description = a->description;
350  }
351  if ( j->error() ) {
352  resultCode = ResultCodeJobError;
353  errorString = j->errorString();
354  kError() << errorString;
355  if ( mShowDialogsOnError ) {
356  KMessageBox::sorry( change->parentWidget,
357  i18n( "Error while trying to delete calendar item. Error was: %1",
358  errorString ) );
359  }
360 
361  foreach( const Item &item, items ) {
362  // Werent deleted due to error
363  mDeletedItemIds.remove( item.id() );
364  }
365  } else { // success
366  if ( change->recordToHistory ) {
367  Q_ASSERT( mHistory );
368  mHistory->recordDeletions( items, description, change->atomicOperationId );
369  }
370 
371  handleInvitationsAfterChange( change );
372  }
373 
374  change->errorString = errorString;
375  change->resultCode = resultCode;
376  // puff, change finally goes out of scope, and emits the incidenceDeleted signal.
377 }
378 
379 void IncidenceChanger::Private::handleModifyJobResult( KJob *job )
380 {
381  QString errorString;
382  ResultCode resultCode = ResultCodeSuccess;
383  Change::Ptr change = mChangeForJob.take( job );
384  mChangeById.remove( change->id );
385 
386  const ItemModifyJob *j = qobject_cast<const ItemModifyJob*>( job );
387  const Item item = j->item();
388  Q_ASSERT( mDirtyFieldsByJob.contains( job ) );
389  item.payload<KCalCore::Incidence::Ptr>()->setDirtyFields( mDirtyFieldsByJob.value( job ) );
390  const QSet<KCalCore::IncidenceBase::Field> dirtyFields = mDirtyFieldsByJob.value( job );
391  QString description;
392  if ( change->atomicOperationId != 0 ) {
393  AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
394  a->numCompletedChanges++;
395  change->completed = true;
396  description = a->description;
397  }
398  if ( j->error() ) {
399  if ( deleteAlreadyCalled( item.id() ) ) {
400  // User deleted the item almost at the same time he changed it. We could just return success
401  // but the delete is probably already recorded to History, and that would make undo not work
402  // in the proper order.
403  resultCode = ResultCodeAlreadyDeleted;
404  errorString = j->errorString();
405  kWarning() << "Trying to change item " << item.id() << " while deletion is in progress.";
406  } else {
407  resultCode = ResultCodeJobError;
408  errorString = j->errorString();
409  kError() << errorString;
410  }
411  if ( mShowDialogsOnError ) {
412  KMessageBox::sorry( change->parentWidget,
413  i18n( "Error while trying to modify calendar item. Error was: %1",
414  errorString ) );
415  }
416  } else { // success
417  ConflictPreventer::self()->mLatestRevisionByItemId[item.id()] = item.revision();
418  change->newItem = item;
419  if ( change->recordToHistory && !change->originalItems.isEmpty() ) {
420  Q_ASSERT( change->originalItems.count() == 1 );
421  mHistory->recordModification( change->originalItems.first(), item,
422  description, change->atomicOperationId );
423  }
424 
425  handleInvitationsAfterChange( change );
426  }
427 
428  change->errorString = errorString;
429  change->resultCode = resultCode;
430  // puff, change finally goes out of scope, and emits the incidenceModified signal.
431 
432  QMetaObject::invokeMethod( this, "performNextModification",
433  Qt::QueuedConnection,
434  Q_ARG( Akonadi::Item::Id, item.id() ) );
435 }
436 
437 bool IncidenceChanger::Private::deleteAlreadyCalled( Akonadi::Item::Id id ) const
438 {
439  return mDeletedItemIds.contains( id );
440 }
441 
442 bool IncidenceChanger::Private::handleInvitationsBeforeChange( const Change::Ptr &change )
443 {
444  bool result = true;
445  if ( mGroupwareCommunication ) {
446  InvitationHandlerHelper handler( change->parentWidget ); // TODO make async
447  if ( mInvitationStatusByAtomicOperation.contains( change->atomicOperationId ) ) {
448  handler.setDefaultAction( actionFromStatus( mInvitationStatusByAtomicOperation.value( change->atomicOperationId ) ) );
449  }
450 
451  switch( change->type ) {
452  case IncidenceChanger::ChangeTypeCreate:
453  // nothing needs to be done
454  break;
455  case IncidenceChanger::ChangeTypeDelete:
456  {
457  InvitationHandlerHelper::SendResult status;
458  foreach( const Akonadi::Item &item, change->originalItems ) {
459  Q_ASSERT( item.hasPayload() );
460  Incidence::Ptr incidence = item.payload<KCalCore::Incidence::Ptr>();
461  if ( !incidence->supportsGroupwareCommunication() )
462  continue;
463  status = handler.sendIncidenceDeletedMessage( KCalCore::iTIPCancel, incidence );
464  if ( change->atomicOperationId ) {
465  mInvitationStatusByAtomicOperation.insert( change->atomicOperationId, status );
466  }
467  result = status != InvitationHandlerHelper::ResultFailAbortUpdate;
468  //TODO: with some status we want to break immediately
469  }
470  }
471  break;
472  case IncidenceChanger::ChangeTypeModify:
473  {
474  if ( !change->originalItems.isEmpty() ) {
475  Q_ASSERT( change->originalItems.count() == 1 );
476  Incidence::Ptr oldIncidence = change->originalItems.first().payload<KCalCore::Incidence::Ptr>();
477  Incidence::Ptr newIncidence = change->newItem.payload<KCalCore::Incidence::Ptr>();
478 
479  if ( oldIncidence->supportsGroupwareCommunication() ) {
480  const bool modify = handler.handleIncidenceAboutToBeModified( newIncidence );
481  if ( !modify ) {
482  if ( newIncidence->type() == oldIncidence->type() ) {
483  IncidenceBase *i1 = newIncidence.data();
484  IncidenceBase *i2 = oldIncidence.data();
485  *i1 = *i2;
486  }
487  result = false;
488  }
489  }
490  }
491  }
492  break;
493  default:
494  Q_ASSERT( false );
495  result = false;
496  }
497  }
498  return result;
499 }
500 
501 bool IncidenceChanger::Private::handleInvitationsAfterChange( const Change::Ptr &change )
502 {
503  if ( mGroupwareCommunication ) {
504  InvitationHandlerHelper handler( change->parentWidget ); // TODO make async
505  switch( change->type ) {
506  case IncidenceChanger::ChangeTypeCreate:
507  {
508  Incidence::Ptr incidence = change->newItem.payload<KCalCore::Incidence::Ptr>();
509  if ( incidence->supportsGroupwareCommunication() ) {
510  const InvitationHandlerHelper::SendResult status =
511  handler.sendIncidenceCreatedMessage( KCalCore::iTIPRequest, incidence );
512 
513  if ( status == InvitationHandlerHelper::ResultFailAbortUpdate ) {
514  kError() << "Sending invitations failed, but did not delete the incidence";
515  }
516 
517  const uint atomicOperationId = change->atomicOperationId;
518  if ( atomicOperationId != 0 ) {
519  mInvitationStatusByAtomicOperation.insert( atomicOperationId, status );
520  }
521  }
522  }
523  break;
524  case IncidenceChanger::ChangeTypeDelete:
525  {
526  foreach( const Akonadi::Item &item, change->originalItems ) {
527  Q_ASSERT( item.hasPayload() );
528  Incidence::Ptr incidence = item.payload<KCalCore::Incidence::Ptr>();
529  Q_ASSERT( incidence );
530  if ( !incidence->supportsGroupwareCommunication() )
531  continue;
532 
533  if ( !Akonadi::CalendarUtils::thatIsMe( incidence->organizer()->email() ) ) {
534  const QStringList myEmails = Akonadi::CalendarUtils::allEmails();
535  bool notifyOrganizer = false;
536  for ( QStringList::ConstIterator it = myEmails.begin(); it != myEmails.end(); ++it ) {
537  const QString email = *it;
538  KCalCore::Attendee::Ptr me( incidence->attendeeByMail( email ) );
539  if ( me ) {
540  if ( me->status() == KCalCore::Attendee::Accepted ||
541  me->status() == KCalCore::Attendee::Delegated ) {
542  notifyOrganizer = true;
543  }
544  KCalCore::Attendee::Ptr newMe( new KCalCore::Attendee( *me ) );
545  newMe->setStatus( KCalCore::Attendee::Declined );
546  incidence->clearAttendees();
547  incidence->addAttendee( newMe );
548  break;
549  }
550  }
551 
552  if ( notifyOrganizer ) {
553  MailScheduler scheduler; // TODO make async
554  scheduler.performTransaction( incidence, KCalCore::iTIPReply );
555  }
556  }
557  }
558  }
559  break;
560  case IncidenceChanger::ChangeTypeModify:
561  {
562  if ( !change->originalItems.isEmpty() ) {
563  Q_ASSERT( change->originalItems.count() == 1 );
564  Incidence::Ptr oldIncidence = change->originalItems.first().payload<KCalCore::Incidence::Ptr>();
565  Incidence::Ptr newIncidence = change->newItem.payload<KCalCore::Incidence::Ptr>();
566  if ( newIncidence->supportsGroupwareCommunication() ) {
567  if ( mInvitationStatusByAtomicOperation.contains( change->atomicOperationId ) ) {
568  handler.setDefaultAction( actionFromStatus( mInvitationStatusByAtomicOperation.value( change->atomicOperationId ) ) );
569  }
570  const bool attendeeStatusChanged = myAttendeeStatusChanged( newIncidence,
571  oldIncidence,
572  Akonadi::CalendarUtils::allEmails() );
573  InvitationHandlerHelper::SendResult status = handler.sendIncidenceModifiedMessage( KCalCore::iTIPRequest,
574  newIncidence,
575  attendeeStatusChanged );
576 
577  if ( change->atomicOperationId != 0 ) {
578  mInvitationStatusByAtomicOperation.insert( change->atomicOperationId, status );
579  }
580  }
581  }
582  }
583  break;
584  default:
585  Q_ASSERT( false );
586  return false;
587  }
588  }
589  return true;
590 }
591 
593 bool IncidenceChanger::Private::myAttendeeStatusChanged( const Incidence::Ptr &newInc,
594  const Incidence::Ptr &oldInc,
595  const QStringList &myEmails )
596 {
597  Q_ASSERT( newInc );
598  Q_ASSERT( oldInc );
599  const Attendee::Ptr oldMe = oldInc->attendeeByMails( myEmails );
600  const Attendee::Ptr newMe = newInc->attendeeByMails( myEmails );
601 
602  return oldMe && newMe && oldMe->status() != newMe->status();
603 }
604 
605 IncidenceChanger::IncidenceChanger( QObject *parent ) : QObject( parent )
606  , d( new Private( true, this ) )
607 {
608 }
609 
610 IncidenceChanger::IncidenceChanger( bool enableHistory, QObject *parent ) : QObject( parent )
611  , d( new Private( enableHistory, this ) )
612 {
613 }
614 
615 IncidenceChanger::~IncidenceChanger()
616 {
617  delete d;
618 }
619 
620 int IncidenceChanger::createIncidence( const Incidence::Ptr &incidence,
621  const Collection &collection,
622  QWidget *parent )
623 {
624  //kDebug();
625  if ( !incidence ) {
626  kWarning() << "An invalid payload is not allowed.";
627  d->cancelTransaction();
628  return -1;
629  }
630 
631  const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
632 
633  const Change::Ptr change( new CreationChange( this, ++d->mLatestChangeId,
634  atomicOperationId, parent ) );
635  Collection collectionToUse;
636 
637  const int changeId = change->id;
638  Q_ASSERT( !( d->mBatchOperationInProgress && !d->mAtomicOperations.contains( atomicOperationId ) ) );
639  if ( d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback() ) {
640  const QString errorMessage = d->showErrorDialog( ResultCodeRolledback, parent );
641  kWarning() << errorMessage;
642 
643  change->resultCode = ResultCodeRolledback;
644  change->errorString = errorMessage;
645  d->cleanupTransaction();
646  return changeId;
647  }
648 
649  d->handleInvitationsBeforeChange( change );
650 
651  if ( collection.isValid() && d->hasRights( collection, ChangeTypeCreate ) ) {
652  // The collection passed always has priority
653  collectionToUse = collection;
654  } else {
655  switch( d->mDestinationPolicy ) {
656  case DestinationPolicyDefault:
657  if ( d->mDefaultCollection.isValid() &&
658  d->hasRights( d->mDefaultCollection, ChangeTypeCreate ) ) {
659  collectionToUse = d->mDefaultCollection;
660  break;
661  }
662  kWarning() << "Destination policy is to use the default collection."
663  << "But it's invalid or doesn't have proper ACLs."
664  << "isValid = " << d->mDefaultCollection.isValid()
665  << "has ACLs = " << d->hasRights( d->mDefaultCollection,
666  ChangeTypeCreate );
667  // else fallthrough, and ask the user.
668  case DestinationPolicyAsk:
669  {
670  int dialogCode;
671  const QStringList mimeTypes( incidence->mimeType() );
672  collectionToUse = selectCollection( parent, dialogCode /*by-ref*/, mimeTypes,
673  d->mDefaultCollection );
674  if ( dialogCode != QDialog::Accepted ) {
675  kDebug() << "User canceled collection choosing";
676  change->resultCode = ResultCodeUserCanceled;
677  d->cancelTransaction();
678  return changeId;
679  }
680 
681  if ( collectionToUse.isValid() && !d->hasRights( collectionToUse, ChangeTypeCreate ) ) {
682  kWarning() << "No ACLs for incidence creation";
683  const QString errorMessage = d->showErrorDialog( ResultCodePermissions, parent );
684  change->resultCode = ResultCodePermissions;
685  change->errorString = errorMessage;
686  d->cancelTransaction();
687  return changeId;
688  }
689 
690  // TODO: add unit test for these two situations after reviewing API
691  if ( !collectionToUse.isValid() ) {
692  kError() << "Invalid collection selected. Can't create incidence.";
693  change->resultCode = ResultCodeInvalidUserCollection;
694  const QString errorString = d->showErrorDialog( ResultCodeInvalidUserCollection, parent );
695  change->errorString = errorString;
696  d->cancelTransaction();
697  return changeId;
698  }
699  }
700  break;
701  case DestinationPolicyNeverAsk:
702  {
703  const bool hasRights = d->hasRights( d->mDefaultCollection, ChangeTypeCreate );
704  if ( d->mDefaultCollection.isValid() && hasRights ) {
705  collectionToUse = d->mDefaultCollection;
706  } else {
707  const QString errorString = d->showErrorDialog( ResultCodeInvalidDefaultCollection, parent );
708  kError() << errorString << "; rights are " << hasRights;
709  change->resultCode = hasRights ? ResultCodeInvalidDefaultCollection :
710  ResultCodePermissions;
711  change->errorString = errorString;
712  d->cancelTransaction();
713  return changeId;
714  }
715  }
716  break;
717  default:
718  // Never happens
719  Q_ASSERT_X( false, "createIncidence()", "unknown destination policy" );
720  d->cancelTransaction();
721  return -1;
722  }
723  }
724 
725  d->mLastCollectionUsed = collectionToUse;
726 
727  Item item;
728  item.setPayload<Incidence::Ptr>( incidence );
729  item.setMimeType( incidence->mimeType() );
730 
731  ItemCreateJob *createJob = new ItemCreateJob( item, collectionToUse, d->parentJob( change ) );
732  d->mChangeForJob.insert( createJob, change );
733 
734  if ( d->mBatchOperationInProgress ) {
735  AtomicOperation *atomic = d->mAtomicOperations[d->mLatestAtomicOperationId];
736  Q_ASSERT( atomic );
737  atomic->addChange( change );
738  }
739 
740  // QueuedConnection because of possible sync exec calls.
741  connect( createJob, SIGNAL(result(KJob*)),
742  d, SLOT(handleCreateJobResult(KJob*)), Qt::QueuedConnection );
743 
744  d->mChangeById.insert( changeId, change );
745  return change->id;
746 }
747 
748 int IncidenceChanger::deleteIncidence( const Item &item, QWidget *parent )
749 {
750  Item::List list;
751  list.append( item );
752 
753  return deleteIncidences( list, parent );
754 }
755 
756 int IncidenceChanger::deleteIncidences( const Item::List &items, QWidget *parent )
757 {
758  //kDebug();
759  if ( items.isEmpty() ) {
760  kError() << "Delete what?";
761  d->cancelTransaction();
762  return -1;
763  }
764 
765  foreach( const Item &item, items ) {
766  if ( !item.isValid() ) {
767  kError() << "Items must be valid!";
768  d->cancelTransaction();
769  return -1;
770  }
771  }
772 
773  const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
774  const int changeId = ++d->mLatestChangeId;
775  const Change::Ptr change( new DeletionChange( this, changeId, atomicOperationId, parent ) );
776 
777  foreach( const Item &item, items ) {
778  if ( !d->hasRights( item.parentCollection(), ChangeTypeDelete ) ) {
779  kWarning() << "Item " << item.id() << " can't be deleted due to ACL restrictions";
780  const QString errorString = d->showErrorDialog( ResultCodePermissions, parent );
781  change->resultCode = ResultCodePermissions;
782  change->errorString = errorString;
783  d->cancelTransaction();
784  return changeId;
785  }
786  }
787 
788  if ( !d->allowAtomicOperation( atomicOperationId, change ) ) {
789  const QString errorString = d->showErrorDialog( ResultCodeDuplicateId, parent );
790  change->resultCode = ResultCodeDuplicateId;
791  change->errorString = errorString;
792  kWarning() << errorString;
793  d->cancelTransaction();
794  return changeId;
795  }
796 
797  Item::List itemsToDelete;
798  foreach( const Item &item, items ) {
799  if ( d->deleteAlreadyCalled( item.id() ) ) {
800  // IncidenceChanger::deleteIncidence() called twice, ignore this one.
801  kDebug() << "Item " << item.id() << " already deleted or being deleted, skipping";
802  } else {
803  itemsToDelete.append( item );
804  }
805  }
806 
807  if ( d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback() ) {
808  const QString errorMessage = d->showErrorDialog( ResultCodeRolledback, parent );
809  change->resultCode = ResultCodeRolledback;
810  change->errorString = errorMessage;
811  kError() << errorMessage;
812  d->cleanupTransaction();
813  return changeId;
814  }
815 
816  if ( itemsToDelete.isEmpty() ) {
817  QVector<Akonadi::Item::Id> itemIdList;
818  itemIdList.append( Item().id() );
819  kDebug() << "Items already deleted or being deleted, skipping";
820  const QString errorMessage =
821  i18n( "That calendar item was already deleted, or currently being deleted." );
822  // Queued emit because return must be executed first, otherwise caller won't know this workId
823  change->resultCode = ResultCodeAlreadyDeleted;
824  change->errorString = errorMessage;
825  d->cancelTransaction();
826  kWarning() << errorMessage;
827  return changeId;
828  }
829 
830  d->handleInvitationsBeforeChange( change );
831 
832  ItemDeleteJob *deleteJob = new ItemDeleteJob( itemsToDelete, d->parentJob( change ) );
833  d->mChangeForJob.insert( deleteJob, change );
834  d->mChangeById.insert( changeId, change );
835 
836  if ( d->mBatchOperationInProgress ) {
837  AtomicOperation *atomic = d->mAtomicOperations[atomicOperationId];
838  Q_ASSERT( atomic );
839  atomic->addChange( change );
840  }
841 
842  foreach( const Item &item, itemsToDelete ) {
843  d->mDeletedItemIds << item.id();
844  }
845 
846  // Do some cleanup
847  if ( d->mDeletedItemIds.count() > 100 )
848  d->mDeletedItemIds.remove( 0, 50 );
849 
850  // QueuedConnection because of possible sync exec calls.
851  connect( deleteJob, SIGNAL(result(KJob*)),
852  d, SLOT(handleDeleteJobResult(KJob*)), Qt::QueuedConnection );
853 
854  return changeId;
855 }
856 
857 int IncidenceChanger::modifyIncidence( const Item &changedItem,
858  const KCalCore::Incidence::Ptr &originalPayload,
859  QWidget *parent )
860 {
861  if ( !changedItem.isValid() || !changedItem.hasPayload<Incidence::Ptr>() ) {
862  kWarning() << "An invalid item or payload is not allowed.";
863  d->cancelTransaction();
864  return -1;
865  }
866 
867  if ( !d->hasRights( changedItem.parentCollection(), ChangeTypeModify ) ) {
868  kWarning() << "Item " << changedItem.id() << " can't be deleted due to ACL restrictions";
869  const int changeId = ++d->mLatestChangeId;
870  const QString errorString = d->showErrorDialog( ResultCodePermissions, parent );
871  emitModifyFinished( this, changeId, changedItem, ResultCodePermissions, errorString );
872  d->cancelTransaction();
873  return changeId;
874  }
875 
876  const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
877  const int changeId = ++d->mLatestChangeId;
878  ModificationChange *modificationChange = new ModificationChange( this, changeId,
879  atomicOperationId, parent );
880  Change::Ptr change( modificationChange );
881 
882  if ( originalPayload ) {
883  Item originalItem( changedItem );
884  originalItem.setPayload<KCalCore::Incidence::Ptr>( originalPayload );
885  modificationChange->originalItems << originalItem;
886  }
887 
888  modificationChange->newItem = changedItem;
889  d->mChangeById.insert( changeId, change );
890 
891  if ( !d->allowAtomicOperation( atomicOperationId, change ) ) {
892  const QString errorString = d->showErrorDialog( ResultCodeDuplicateId, parent );
893  change->resultCode = ResultCodeDuplicateId;
894  change->errorString = errorString;
895  d->cancelTransaction();
896  kWarning() << "Atomic operation now allowed";
897  return changeId;
898  }
899 
900  if ( d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback() ) {
901  const QString errorMessage = d->showErrorDialog( ResultCodeRolledback, parent );
902  kError() << errorMessage;
903  d->cleanupTransaction();
904  emitModifyFinished( this, changeId, changedItem, ResultCodeRolledback, errorMessage );
905  } else {
906  d->performModification( change );
907  }
908 
909  return changeId;
910 }
911 
912 void IncidenceChanger::Private::performModification( Change::Ptr change )
913 {
914  const Item::Id id = change->newItem.id();
915  Akonadi::Item &newItem = change->newItem;
916  Q_ASSERT( newItem.isValid() );
917  Q_ASSERT( newItem.hasPayload<Incidence::Ptr>() );
918 
919  const int changeId = change->id;
920 
921  if ( deleteAlreadyCalled( id ) ) {
922  // IncidenceChanger::deleteIncidence() called twice, ignore this one.
923  kDebug() << "Item " << id << " already deleted or being deleted, skipping";
924 
925  // Queued emit because return must be executed first, otherwise caller won't know this workId
926  emitModifyFinished( q, change->id, newItem, ResultCodeAlreadyDeleted,
927  i18n( "That calendar item was already deleted, or currently being deleted." ) );
928  return;
929  }
930 
931  const uint atomicOperationId = change->atomicOperationId;
932  const bool hasAtomicOperationId = atomicOperationId != 0;
933  if ( hasAtomicOperationId &&
934  mAtomicOperations[atomicOperationId]->rolledback() ) {
935  const QString errorMessage = showErrorDialog( ResultCodeRolledback, 0 );
936  kError() << errorMessage;
937  emitModifyFinished( q, changeId, newItem, ResultCodeRolledback, errorMessage );
938  return;
939  }
940 
941  handleInvitationsBeforeChange( change );
942 
943  QHash<Akonadi::Item::Id, int> &latestRevisionByItemId =
944  ConflictPreventer::self()->mLatestRevisionByItemId;
945  if ( latestRevisionByItemId.contains( id ) &&
946  latestRevisionByItemId[id] > newItem.revision() ) {
947  /* When a ItemModifyJob ends, the application can still modify the old items if the user
948  * is quick because the ETM wasn't updated yet, and we'll get a STORE error, because
949  * we are not modifying the latest revision.
950  *
951  * When a job ends, we keep the new revision in mLatestRevisionByItemId
952  * so we can update the item's revision
953  */
954  newItem.setRevision( latestRevisionByItemId[id] );
955  }
956 
957  Incidence::Ptr incidence = newItem.payload<Incidence::Ptr>();
958  { // increment revision ( KCalCore revision, not akonadi )
959  const int revision = incidence->revision();
960  incidence->setRevision( revision + 1 );
961  }
962 
963  // Dav Fix
964  // Don't write back remote revision since we can't make sure it is the current one
965  newItem.setRemoteRevision( QString() );
966 
967  if ( mModificationsInProgress.contains( newItem.id() ) ) {
968  // There's already a ItemModifyJob running for this item ID
969  // Let's wait for it to end.
970  queueModification( change );
971  } else {
972  ItemModifyJob *modifyJob = new ItemModifyJob( newItem, parentJob( change ) );
973  mChangeForJob.insert( modifyJob, change );
974  mDirtyFieldsByJob.insert( modifyJob, incidence->dirtyFields() );
975 
976  if ( hasAtomicOperationId ) {
977  AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
978  Q_ASSERT( atomic );
979  atomic->addChange( change );
980  }
981 
982  mModificationsInProgress[newItem.id()] = change;
983  // QueuedConnection because of possible sync exec calls.
984  connect( modifyJob, SIGNAL(result(KJob*)),
985  SLOT(handleModifyJobResult(KJob*)), Qt::QueuedConnection );
986  }
987 }
988 
989 void IncidenceChanger::startAtomicOperation( const QString &operationDescription )
990 {
991  if ( d->mBatchOperationInProgress ) {
992  kDebug() << "An atomic operation is already in progress.";
993  return;
994  }
995 
996  ++d->mLatestAtomicOperationId;
997  d->mBatchOperationInProgress = true;
998 
999  AtomicOperation *atomicOperation = new AtomicOperation( d->mLatestAtomicOperationId );
1000  atomicOperation->description = operationDescription;
1001  d->mAtomicOperations.insert( d->mLatestAtomicOperationId, atomicOperation );
1002  d->mAtomicOperationByTransaction.insert( atomicOperation->transaction, d->mLatestAtomicOperationId );
1003 
1004  d->connect( atomicOperation->transaction, SIGNAL(result(KJob*)),
1005  d, SLOT(handleTransactionJobResult(KJob*)) );
1006 }
1007 
1008 void IncidenceChanger::endAtomicOperation()
1009 {
1010  if ( !d->mBatchOperationInProgress ) {
1011  kWarning() << "No atomic operation is in progress.";
1012  return;
1013  }
1014 
1015  Q_ASSERT_X( d->mLatestAtomicOperationId != 0,
1016  "IncidenceChanger::endAtomicOperation()",
1017  "Call startAtomicOperation() first." );
1018 
1019  Q_ASSERT( d->mAtomicOperations.contains(d->mLatestAtomicOperationId) );
1020  AtomicOperation *atomicOperation = d->mAtomicOperations[d->mLatestAtomicOperationId];
1021  Q_ASSERT( atomicOperation );
1022  atomicOperation->endCalled = true;
1023 
1024  const bool allJobsCompleted = !atomicOperation->pendingJobs();
1025 
1026  if ( allJobsCompleted && atomicOperation->rolledback() &&
1027  atomicOperation->transactionCompleted ) {
1028  // The transaction job already completed, we can cleanup:
1029  delete d->mAtomicOperations.take( d->mLatestAtomicOperationId );
1030  d->mBatchOperationInProgress = false;
1031  }/* else if ( allJobsCompleted ) {
1032  Q_ASSERT( atomicOperation->transaction );
1033  atomicOperation->transaction->commit(); we using autocommit now
1034  }*/
1035 }
1036 
1037 void IncidenceChanger::setShowDialogsOnError( bool enable )
1038 {
1039  d->mShowDialogsOnError = enable;
1040 }
1041 
1042 bool IncidenceChanger::showDialogsOnError() const
1043 {
1044  return d->mShowDialogsOnError;
1045 }
1046 
1047 void IncidenceChanger::setRespectsCollectionRights( bool respects )
1048 {
1049  d->mRespectsCollectionRights = respects;
1050 }
1051 
1052 bool IncidenceChanger::respectsCollectionRights() const
1053 {
1054  return d->mRespectsCollectionRights;
1055 }
1056 
1057 void IncidenceChanger::setDestinationPolicy( IncidenceChanger::DestinationPolicy destinationPolicy )
1058 {
1059  d->mDestinationPolicy = destinationPolicy;
1060 }
1061 
1062 IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy() const
1063 {
1064  return d->mDestinationPolicy;
1065 }
1066 
1067 void IncidenceChanger::setDefaultCollection( const Akonadi::Collection &collection )
1068 {
1069  d->mDefaultCollection = collection;
1070 }
1071 
1072 Collection IncidenceChanger::defaultCollection() const
1073 {
1074  return d->mDefaultCollection;
1075 }
1076 
1077 bool IncidenceChanger::historyEnabled() const
1078 {
1079  return d->mUseHistory;
1080 }
1081 
1082 void IncidenceChanger::setHistoryEnabled( bool enable )
1083 {
1084  if ( d->mUseHistory != enable ) {
1085  d->mUseHistory = enable;
1086  if ( enable && !d->mHistory )
1087  d->mHistory = new History( this );
1088  }
1089 }
1090 
1091 History* IncidenceChanger::history() const
1092 {
1093  return d->mHistory;
1094 }
1095 
1096 bool IncidenceChanger::deletedRecently( Akonadi::Item::Id id ) const
1097 {
1098  return d->deleteAlreadyCalled( id );
1099 }
1100 
1101 void IncidenceChanger::setGroupwareCommunication( bool enabled )
1102 {
1103  d->mGroupwareCommunication = enabled;
1104 }
1105 
1106 bool IncidenceChanger::groupwareCommunication() const
1107 {
1108  return d->mGroupwareCommunication;
1109 }
1110 
1111 Akonadi::Collection IncidenceChanger::lastCollectionUsed() const
1112 {
1113  return d->mLastCollectionUsed;
1114 }
1115 
1116 QString IncidenceChanger::Private::showErrorDialog( IncidenceChanger::ResultCode resultCode,
1117  QWidget *parent )
1118 {
1119  QString errorString;
1120  switch( resultCode ) {
1121  case IncidenceChanger::ResultCodePermissions:
1122  errorString = i18n( "Operation can not be performed due to ACL restrictions" );
1123  break;
1124  case IncidenceChanger::ResultCodeInvalidUserCollection:
1125  errorString = i18n( "The chosen collection is invalid" );
1126  break;
1127  case IncidenceChanger::ResultCodeInvalidDefaultCollection:
1128  errorString = i18n( "Default collection is invalid or doesn't have proper ACLs"
1129  " and DestinationPolicyNeverAsk was used" );
1130  break;
1131  case IncidenceChanger::ResultCodeDuplicateId:
1132  errorString = i18n( "Duplicate item id in a group operation");
1133  break;
1134  case IncidenceChanger::ResultCodeRolledback:
1135  errorString = i18n( "One change belonging to a group of changes failed. "
1136  "All changes are being rolled back." );
1137  break;
1138  default:
1139  Q_ASSERT( false );
1140  return QString( i18n( "Unknown error" ) );
1141  }
1142 
1143  if ( mShowDialogsOnError ) {
1144  KMessageBox::sorry( parent, errorString );
1145  }
1146 
1147  return errorString;
1148 }
1149 
1150 void IncidenceChanger::Private::cancelTransaction()
1151 {
1152  if ( mBatchOperationInProgress ) {
1153  mAtomicOperations[mLatestAtomicOperationId]->setRolledback();
1154  }
1155 }
1156 
1157 void IncidenceChanger::Private::cleanupTransaction()
1158 {
1159  Q_ASSERT( mAtomicOperations.contains(mLatestAtomicOperationId) );
1160  AtomicOperation *operation = mAtomicOperations[mLatestAtomicOperationId];
1161  Q_ASSERT( operation );
1162  Q_ASSERT( operation->rolledback() );
1163  if ( !operation->pendingJobs() && operation->endCalled && operation->transactionCompleted ) {
1164  delete mAtomicOperations.take(mLatestAtomicOperationId);
1165  mBatchOperationInProgress = false;
1166  }
1167 }
1168 
1169 bool IncidenceChanger::Private::allowAtomicOperation( int atomicOperationId,
1170  const Change::Ptr &change ) const
1171 {
1172  bool allow = true;
1173  if ( atomicOperationId > 0 ) {
1174  Q_ASSERT( mAtomicOperations.contains( atomicOperationId ) );
1175  AtomicOperation *operation = mAtomicOperations.value( atomicOperationId );
1176 
1177  if ( change->type == ChangeTypeCreate ) {
1178  allow = true;
1179  } else if ( change->type == ChangeTypeModify ) {
1180  allow = !operation->mItemIdsInOperation.contains( change->newItem.id() );
1181  } else if ( change->type == ChangeTypeDelete ) {
1182  DeletionChange::Ptr deletion = change.staticCast<DeletionChange>();
1183  foreach( Akonadi::Item::Id id, deletion->mItemIds ) {
1184  if ( operation->mItemIdsInOperation.contains( id ) ) {
1185  allow = false;
1186  break;
1187  }
1188  }
1189  }
1190  }
1191 
1192  if ( !allow ) {
1193  kWarning() << "Each change belonging to a group operation"
1194  << "must have a different Akonadi::Item::Id";
1195  }
1196 
1197  return allow;
1198 }
1199 
1201 void ModificationChange::emitCompletionSignal()
1202 {
1203  emitModifyFinished( changer, id, newItem, resultCode, errorString );
1204 }
1205 
1207 void CreationChange::emitCompletionSignal()
1208 {
1209  // Does a queued emit, with QMetaObject::invokeMethod
1210  emitCreateFinished( changer, id, newItem, resultCode, errorString );
1211 }
1212 
1214 void DeletionChange::emitCompletionSignal()
1215 {
1216  emitDeleteFinished( changer, id, mItemIds, resultCode, errorString );
1217 }
1218 
This file is part of the KDE documentation.
Documentation copyright © 1996-2013 The KDE developers.
Generated on Sat Jul 13 2013 01:27:37 by doxygen 1.8.3.1 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

akonadi

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

kdepimlibs-4.10.5 API Reference

Skip menu "kdepimlibs-4.10.5 API Reference"
  • akonadi
  •   contact
  •   kmime
  •   socialutils
  • kabc
  • kalarmcal
  • kblog
  • kcal
  • kcalcore
  • kcalutils
  • kholidays
  • kimap
  • kioslave
  •   imap4
  •   mbox
  •   nntp
  • kldap
  • kmbox
  • kmime
  • kontactinterface
  • kpimidentities
  • kpimtextedit
  • kpimutils
  • kresources
  • ktnef
  • kxmlrpcclient
  • mailtransport
  • microblog
  • qgpgme
  • syndication
  •   atom
  •   rdf
  •   rss2
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