KCalUtils Library
scheduler.cpp
00001 /* 00002 This file is part of the kcalutils library. 00003 00004 Copyright (c) 2001,2004 Cornelius Schumacher <schumacher@kde.org> 00005 Copyright (C) 2004 Reinhold Kainhofer <reinhold@kainhofer.com> 00006 00007 This library is free software; you can redistribute it and/or 00008 modify it under the terms of the GNU Library General Public 00009 License as published by the Free Software Foundation; either 00010 version 2 of the License, or (at your option) any later version. 00011 00012 This library is distributed in the hope that it will be useful, 00013 but WITHOUT ANY WARRANTY; without even the implied warranty of 00014 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00015 Library General Public License for more details. 00016 00017 You should have received a copy of the GNU Library General Public License 00018 along with this library; see the file COPYING.LIB. If not, write to 00019 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 00020 Boston, MA 02110-1301, USA. 00021 */ 00022 #include "scheduler.h" 00023 #include "stringify.h" 00024 00025 #include <kcalcore/icalformat.h> 00026 #include <kcalcore/freebusycache.h> 00027 using namespace KCalCore; 00028 00029 #include <KDebug> 00030 #include <KLocale> 00031 #include <KMessageBox> 00032 00033 using namespace KCalUtils; 00034 00035 //@cond PRIVATE 00036 struct KCalUtils::Scheduler::Private 00037 { 00038 public: 00039 Private() : mFreeBusyCache( 0 ) 00040 { 00041 } 00042 FreeBusyCache *mFreeBusyCache; 00043 }; 00044 //@endcond 00045 00046 Scheduler::Scheduler( const Calendar::Ptr &calendar ) : d( new KCalUtils::Scheduler::Private ) 00047 { 00048 mCalendar = calendar; 00049 mFormat = new ICalFormat(); 00050 mFormat->setTimeSpec( calendar->timeSpec() ); 00051 } 00052 00053 Scheduler::~Scheduler() 00054 { 00055 delete mFormat; 00056 delete d; 00057 } 00058 00059 void Scheduler::setFreeBusyCache( FreeBusyCache *c ) 00060 { 00061 d->mFreeBusyCache = c; 00062 } 00063 00064 FreeBusyCache *Scheduler::freeBusyCache() const 00065 { 00066 return d->mFreeBusyCache; 00067 } 00068 00069 bool Scheduler::acceptTransaction( const IncidenceBase::Ptr &incidence, iTIPMethod method, 00070 ScheduleMessage::Status status, const QString &email ) 00071 { 00072 kDebug() << "method=" << ScheduleMessage::methodName( method ); //krazy:exclude=kdebug 00073 00074 switch ( method ) { 00075 case iTIPPublish: 00076 return acceptPublish( incidence, status, method ); 00077 case iTIPRequest: 00078 return acceptRequest( incidence, status, email ); 00079 case iTIPAdd: 00080 return acceptAdd( incidence, status ); 00081 case iTIPCancel: 00082 return acceptCancel( incidence, status, email ); 00083 case iTIPDeclineCounter: 00084 return acceptDeclineCounter( incidence, status ); 00085 case iTIPReply: 00086 return acceptReply( incidence, status, method ); 00087 case iTIPRefresh: 00088 return acceptRefresh( incidence, status ); 00089 case iTIPCounter: 00090 return acceptCounter( incidence, status ); 00091 default: 00092 break; 00093 } 00094 deleteTransaction( incidence ); 00095 return false; 00096 } 00097 00098 bool Scheduler::deleteTransaction( const IncidenceBase::Ptr & ) 00099 { 00100 return true; 00101 } 00102 00103 bool Scheduler::acceptPublish( const IncidenceBase::Ptr &newIncBase, ScheduleMessage::Status status, 00104 iTIPMethod method ) 00105 { 00106 if ( newIncBase->type() == IncidenceBase::TypeFreeBusy ) { 00107 return acceptFreeBusy( newIncBase, method ); 00108 } 00109 00110 bool res = false; 00111 00112 kDebug() << "status=" << Stringify::scheduleMessageStatus( status ); //krazy:exclude=kdebug 00113 00114 Incidence::Ptr newInc = newIncBase.staticCast<Incidence>() ; 00115 Incidence::Ptr calInc = mCalendar->incidence( newIncBase->uid() ); 00116 switch ( status ) { 00117 case ScheduleMessage::Unknown: 00118 case ScheduleMessage::PublishNew: 00119 case ScheduleMessage::PublishUpdate: 00120 if ( calInc && newInc ) { 00121 if ( ( newInc->revision() > calInc->revision() ) || 00122 ( newInc->revision() == calInc->revision() && 00123 newInc->lastModified() > calInc->lastModified() ) ) { 00124 const QString oldUid = calInc->uid(); 00125 00126 if ( calInc->type() != newInc->type() ) { 00127 kError() << "assigning different incidence types"; 00128 } else { 00129 IncidenceBase *ci = calInc.data(); 00130 IncidenceBase *ni = newInc.data(); 00131 *ci = *ni; 00132 calInc->setSchedulingID( newInc->uid(), oldUid ); 00133 res = true; 00134 } 00135 } 00136 } 00137 break; 00138 case ScheduleMessage::Obsolete: 00139 res = true; 00140 break; 00141 default: 00142 break; 00143 } 00144 deleteTransaction( newIncBase ); 00145 return res; 00146 } 00147 00148 bool Scheduler::acceptRequest( const IncidenceBase::Ptr &incidence, 00149 ScheduleMessage::Status status, 00150 const QString &email ) 00151 { 00152 Incidence::Ptr inc = incidence.staticCast<Incidence>() ; 00153 if ( !inc ) { 00154 kWarning() << "Accept what?"; 00155 return false; 00156 } 00157 if ( inc->type() == IncidenceBase::TypeFreeBusy ) { 00158 // reply to this request is handled in korganizer's incomingdialog 00159 return true; 00160 } 00161 00162 const Incidence::List existingIncidences = mCalendar->incidencesFromSchedulingID( inc->uid() ); 00163 kDebug() << "status=" << Stringify::scheduleMessageStatus( status ) //krazy:exclude=kdebug 00164 << ": found " << existingIncidences.count() 00165 << " incidences with schedulingID " << inc->schedulingID() 00166 << "; uid was = " << inc->uid(); 00167 00168 if ( existingIncidences.isEmpty() ) { 00169 // Perfectly normal if the incidence doesn't exist. This is probably 00170 // a new invitation. 00171 kDebug() << "incidence not found; calendar = " << mCalendar.data() 00172 << "; incidence count = " << mCalendar->incidences().count(); 00173 } 00174 Incidence::List::ConstIterator incit = existingIncidences.begin(); 00175 for ( ; incit != existingIncidences.end() ; ++incit ) { 00176 Incidence::Ptr existingIncidence = *incit; 00177 kDebug() << "Considering this found event (" 00178 << ( existingIncidence->isReadOnly() ? "readonly" : "readwrite" ) 00179 << ") :" << mFormat->toString( existingIncidence ); 00180 // If it's readonly, we can't possible update it. 00181 if ( existingIncidence->isReadOnly() ) { 00182 continue; 00183 } 00184 if ( existingIncidence->revision() <= inc->revision() ) { 00185 // The new incidence might be an update for the found one 00186 bool isUpdate = true; 00187 // Code for new invitations: 00188 // If you think we could check the value of "status" to be RequestNew: we can't. 00189 // It comes from a similar check inside libical, where the event is compared to 00190 // other events in the calendar. But if we have another version of the event around 00191 // (e.g. shared folder for a group), the status could be RequestNew, Obsolete or Updated. 00192 kDebug() << "looking in " << existingIncidence->uid() << "'s attendees"; 00193 // This is supposed to be a new request, not an update - however we want to update 00194 // the existing one to handle the "clicking more than once on the invitation" case. 00195 // So check the attendee status of the attendee. 00196 const Attendee::List attendees = existingIncidence->attendees(); 00197 Attendee::List::ConstIterator ait; 00198 for ( ait = attendees.begin(); ait != attendees.end(); ++ait ) { 00199 if( (*ait)->email() == email && (*ait)->status() == Attendee::NeedsAction ) { 00200 // This incidence wasn't created by me - it's probably in a shared folder 00201 // and meant for someone else, ignore it. 00202 kDebug() << "ignoring " << existingIncidence->uid() << " since I'm still NeedsAction there"; 00203 isUpdate = false; 00204 break; 00205 } 00206 } 00207 if ( isUpdate ) { 00208 if ( existingIncidence->revision() == inc->revision() && 00209 existingIncidence->lastModified() > inc->lastModified() ) { 00210 // This isn't an update - the found incidence was modified more recently 00211 kDebug() << "This isn't an update - the found incidence was modified more recently"; 00212 deleteTransaction( existingIncidence ); 00213 return false; 00214 } 00215 kDebug() << "replacing existing incidence " << existingIncidence->uid(); 00216 bool res = true; 00217 const QString oldUid = existingIncidence->uid(); 00218 if ( existingIncidence->type() != inc->type() ) { 00219 kError() << "assigning different incidence types"; 00220 res = false; 00221 } else { 00222 IncidenceBase *existingIncidenceBase = existingIncidence.data(); 00223 IncidenceBase *incBase = inc.data(); 00224 *existingIncidenceBase = *incBase; 00225 existingIncidence->setSchedulingID( inc->uid(), oldUid ); 00226 } 00227 deleteTransaction( incidence ); 00228 return res; 00229 } 00230 } else { 00231 // This isn't an update - the found incidence has a bigger revision number 00232 kDebug() << "This isn't an update - the found incidence has a bigger revision number"; 00233 deleteTransaction( incidence ); 00234 return false; 00235 } 00236 } 00237 00238 // Move the uid to be the schedulingID and make a unique UID 00239 inc->setSchedulingID( inc->uid(), CalFormat::createUniqueId() ); 00240 // notify the user in case this is an update and we didn't find the to-be-updated incidence 00241 if ( existingIncidences.count() == 0 && inc->revision() > 0 ) { 00242 KMessageBox::information( 00243 0, 00244 i18nc( "@info", 00245 "<para>You accepted an invitation update, but an earlier version of the " 00246 "item could not be found in your calendar.</para>" 00247 "<para>This may have occurred because:<list>" 00248 "<item>the organizer did not include you in the original invitation</item>" 00249 "<item>you did not accept the original invitation yet</item>" 00250 "<item>you deleted the original invitation from your calendar</item>" 00251 "<item>you no longer have access to the calendar containing the invitation</item>" 00252 "</list></para>" 00253 "<para>This is not a problem, but we thought you should know.</para>" ), 00254 i18nc( "@title", "Cannot find invitation to be updated" ), "AcceptCantFindIncidence" ); 00255 } 00256 kDebug() << "Storing new incidence with scheduling uid=" << inc->schedulingID() 00257 << " and uid=" << inc->uid(); 00258 mCalendar->addIncidence( inc ); 00259 00260 deleteTransaction( incidence ); 00261 return true; 00262 } 00263 00264 bool Scheduler::acceptAdd( const IncidenceBase::Ptr &incidence, 00265 ScheduleMessage::Status /* status */) 00266 { 00267 deleteTransaction( incidence ); 00268 return false; 00269 } 00270 00271 bool Scheduler::acceptCancel( const IncidenceBase::Ptr &incidence, 00272 ScheduleMessage::Status status, 00273 const QString &attendee ) 00274 { 00275 Incidence::Ptr inc = incidence.staticCast<Incidence>(); 00276 if ( !inc ) { 00277 return false; 00278 } 00279 00280 if ( inc->type() == IncidenceBase::TypeFreeBusy ) { 00281 // reply to this request is handled in korganizer's incomingdialog 00282 return true; 00283 } 00284 00285 const Incidence::List existingIncidences = mCalendar->incidencesFromSchedulingID( inc->uid() ); 00286 kDebug() << "Scheduler::acceptCancel=" 00287 << Stringify::scheduleMessageStatus( status ) //krazy2:exclude=kdebug 00288 << ": found " << existingIncidences.count() 00289 << " incidences with schedulingID " << inc->schedulingID(); 00290 00291 bool ret = false; 00292 Incidence::List::ConstIterator incit = existingIncidences.begin(); 00293 for ( ; incit != existingIncidences.end() ; ++incit ) { 00294 Incidence::Ptr i = *incit; 00295 kDebug() << "Considering this found event (" 00296 << ( i->isReadOnly() ? "readonly" : "readwrite" ) 00297 << ") :" << mFormat->toString( i ); 00298 00299 // If it's readonly, we can't possible remove it. 00300 if ( i->isReadOnly() ) { 00301 continue; 00302 } 00303 00304 // Code for new invitations: 00305 // We cannot check the value of "status" to be RequestNew because 00306 // "status" comes from a similar check inside libical, where the event 00307 // is compared to other events in the calendar. But if we have another 00308 // version of the event around (e.g. shared folder for a group), the 00309 // status could be RequestNew, Obsolete or Updated. 00310 kDebug() << "looking in " << i->uid() << "'s attendees"; 00311 00312 // This is supposed to be a new request, not an update - however we want 00313 // to update the existing one to handle the "clicking more than once 00314 // on the invitation" case. So check the attendee status of the attendee. 00315 bool isMine = true; 00316 const Attendee::List attendees = i->attendees(); 00317 Attendee::List::ConstIterator ait; 00318 for ( ait = attendees.begin(); ait != attendees.end(); ++ait ) { 00319 if ( (*ait)->email() == attendee && 00320 (*ait)->status() == Attendee::NeedsAction ) { 00321 // This incidence wasn't created by me - it's probably in a shared 00322 // folder and meant for someone else, ignore it. 00323 kDebug() << "ignoring " << i->uid() 00324 << " since I'm still NeedsAction there"; 00325 isMine = false; 00326 break; 00327 } 00328 } 00329 00330 if ( isMine ) { 00331 kDebug() << "removing existing incidence " << i->uid(); 00332 if ( i->type() == IncidenceBase::TypeEvent ) { 00333 Event::Ptr event = mCalendar->event( i->uid() ); 00334 ret = ( event && mCalendar->deleteEvent( event ) ); 00335 } else if ( i->type() == IncidenceBase::TypeTodo ) { 00336 Todo::Ptr todo = mCalendar->todo( i->uid() ); 00337 ret = ( todo && mCalendar->deleteTodo( todo ) ); 00338 } 00339 deleteTransaction( incidence ); 00340 return ret; 00341 } 00342 } 00343 00344 // in case we didn't find the to-be-removed incidence 00345 if ( existingIncidences.count() > 0 && inc->revision() > 0 ) { 00346 KMessageBox::error( 00347 0, 00348 i18nc( "@info", 00349 "The event or task could not be removed from your calendar. " 00350 "Maybe it has already been deleted or is not owned by you. " 00351 "Or it might belong to a read-only or disabled calendar." ) ); 00352 } 00353 deleteTransaction( incidence ); 00354 return ret; 00355 } 00356 00357 bool Scheduler::acceptDeclineCounter( const IncidenceBase::Ptr &incidence, 00358 ScheduleMessage::Status status ) 00359 { 00360 Q_UNUSED( status ); 00361 deleteTransaction( incidence ); 00362 return false; 00363 } 00364 00365 bool Scheduler::acceptReply( const IncidenceBase::Ptr &incidence, ScheduleMessage::Status status, 00366 iTIPMethod method ) 00367 { 00368 Q_UNUSED( status ); 00369 if ( incidence->type() == IncidenceBase::TypeFreeBusy ) { 00370 return acceptFreeBusy( incidence, method ); 00371 } 00372 bool ret = false; 00373 Event::Ptr ev = mCalendar->event( incidence->uid() ); 00374 Todo::Ptr to = mCalendar->todo( incidence->uid() ); 00375 00376 // try harder to find the correct incidence 00377 if ( !ev && !to ) { 00378 const Incidence::List list = mCalendar->incidences(); 00379 for ( Incidence::List::ConstIterator it=list.constBegin(), end=list.constEnd(); 00380 it != end; ++it ) { 00381 if ( (*it)->schedulingID() == incidence->uid() ) { 00382 ev = ( *it ).dynamicCast<Event>(); 00383 to = ( *it ).dynamicCast<Todo>(); 00384 break; 00385 } 00386 } 00387 } 00388 00389 if ( ev || to ) { 00390 //get matching attendee in calendar 00391 kDebug() << "match found!"; 00392 Attendee::List attendeesIn = incidence->attendees(); 00393 Attendee::List attendeesEv; 00394 Attendee::List attendeesNew; 00395 if ( ev ) { 00396 attendeesEv = ev->attendees(); 00397 } 00398 if ( to ) { 00399 attendeesEv = to->attendees(); 00400 } 00401 Attendee::List::ConstIterator inIt; 00402 Attendee::List::ConstIterator evIt; 00403 for ( inIt = attendeesIn.constBegin(); inIt != attendeesIn.constEnd(); ++inIt ) { 00404 Attendee::Ptr attIn = *inIt; 00405 bool found = false; 00406 for ( evIt = attendeesEv.constBegin(); evIt != attendeesEv.constEnd(); ++evIt ) { 00407 Attendee::Ptr attEv = *evIt; 00408 if ( attIn->email().toLower() == attEv->email().toLower() ) { 00409 //update attendee-info 00410 kDebug() << "update attendee"; 00411 attEv->setStatus( attIn->status() ); 00412 attEv->setDelegate( attIn->delegate() ); 00413 attEv->setDelegator( attIn->delegator() ); 00414 ret = true; 00415 found = true; 00416 } 00417 } 00418 if ( !found && attIn->status() != Attendee::Declined ) { 00419 attendeesNew.append( attIn ); 00420 } 00421 } 00422 00423 bool attendeeAdded = false; 00424 for ( Attendee::List::ConstIterator it = attendeesNew.constBegin(); 00425 it != attendeesNew.constEnd(); ++it ) { 00426 Attendee::Ptr attNew = *it; 00427 QString msg = 00428 i18nc( "@info", "%1 wants to attend %2 but was not invited.", 00429 attNew->fullName(), 00430 ( ev ? ev->summary() : to->summary() ) ); 00431 if ( !attNew->delegator().isEmpty() ) { 00432 msg = i18nc( "@info", "%1 wants to attend %2 on behalf of %3.", 00433 attNew->fullName(), 00434 ( ev ? ev->summary() : to->summary() ), attNew->delegator() ); 00435 } 00436 if ( KMessageBox::questionYesNo( 00437 0, msg, i18nc( "@title", "Uninvited attendee" ), 00438 KGuiItem( i18nc( "@option", "Accept Attendance" ) ), 00439 KGuiItem( i18nc( "@option", "Reject Attendance" ) ) ) != KMessageBox::Yes ) { 00440 Incidence::Ptr cancel = incidence.dynamicCast<Incidence>(); 00441 if ( cancel ) { 00442 cancel->addComment( 00443 i18nc( "@info", 00444 "The organizer rejected your attendance at this meeting." ) ); 00445 } 00446 performTransaction( incidence, iTIPCancel, attNew->fullName() ); 00447 // ### can't delete cancel here because it is aliased to incidence which 00448 // is accessed in the next loop iteration (CID 4232) 00449 // delete cancel; 00450 continue; 00451 } 00452 00453 Attendee::Ptr a( new Attendee( attNew->name(), attNew->email(), attNew->RSVP(), 00454 attNew->status(), attNew->role(), attNew->uid() ) ); 00455 00456 a->setDelegate( attNew->delegate() ); 00457 a->setDelegator( attNew->delegator() ); 00458 if ( ev ) { 00459 ev->addAttendee( a ); 00460 } else if ( to ) { 00461 to->addAttendee( a ); 00462 } 00463 ret = true; 00464 attendeeAdded = true; 00465 } 00466 00467 // send update about new participants 00468 if ( attendeeAdded ) { 00469 bool sendMail = false; 00470 if ( ev || to ) { 00471 if ( KMessageBox::questionYesNo( 00472 0, 00473 i18nc( "@info", 00474 "An attendee was added to the incidence. " 00475 "Do you want to email the attendees an update message?" ), 00476 i18nc( "@title", "Attendee Added" ), 00477 KGuiItem( i18nc( "@option", "Send Messages" ) ), 00478 KGuiItem( i18nc( "@option", "Do Not Send" ) ) ) == KMessageBox::Yes ) { 00479 sendMail = true; 00480 } 00481 } 00482 00483 if ( ev ) { 00484 ev->setRevision( ev->revision() + 1 ); 00485 if ( sendMail ) { 00486 performTransaction( ev, iTIPRequest ); 00487 } 00488 } 00489 if ( to ) { 00490 to->setRevision( to->revision() + 1 ); 00491 if ( sendMail ) { 00492 performTransaction( to, iTIPRequest ); 00493 } 00494 } 00495 } 00496 00497 if ( ret ) { 00498 // We set at least one of the attendees, so the incidence changed 00499 // Note: This should not result in a sequence number bump 00500 if ( ev ) { 00501 ev->updated(); 00502 } else if ( to ) { 00503 to->updated(); 00504 } 00505 } 00506 if ( to ) { 00507 // for VTODO a REPLY can be used to update the completion status of 00508 // a to-do. see RFC2446 3.4.3 00509 Todo::Ptr update = incidence.dynamicCast<Todo>(); 00510 Q_ASSERT( update ); 00511 if ( update && ( to->percentComplete() != update->percentComplete() ) ) { 00512 to->setPercentComplete( update->percentComplete() ); 00513 to->updated(); 00514 } 00515 } 00516 } else { 00517 kError() << "No incidence for scheduling."; 00518 } 00519 00520 if ( ret ) { 00521 deleteTransaction( incidence ); 00522 } 00523 return ret; 00524 } 00525 00526 bool Scheduler::acceptRefresh( const IncidenceBase::Ptr &incidence, ScheduleMessage::Status status ) 00527 { 00528 Q_UNUSED( status ); 00529 // handled in korganizer's IncomingDialog 00530 deleteTransaction( incidence ); 00531 return false; 00532 } 00533 00534 bool Scheduler::acceptCounter( const IncidenceBase::Ptr &incidence, ScheduleMessage::Status status ) 00535 { 00536 Q_UNUSED( status ); 00537 deleteTransaction( incidence ); 00538 return false; 00539 } 00540 00541 bool Scheduler::acceptFreeBusy( const IncidenceBase::Ptr &incidence, iTIPMethod method ) 00542 { 00543 if ( !d->mFreeBusyCache ) { 00544 kError() << "Scheduler: no FreeBusyCache."; 00545 return false; 00546 } 00547 00548 FreeBusy::Ptr freebusy = incidence.staticCast<FreeBusy>(); 00549 00550 kDebug() << "freeBusyDirName:" << freeBusyDir(); 00551 00552 Person::Ptr from; 00553 if( method == iTIPPublish ) { 00554 from = freebusy->organizer(); 00555 } 00556 if ( ( method == iTIPReply ) && ( freebusy->attendeeCount() == 1 ) ) { 00557 Attendee::Ptr attendee = freebusy->attendees().first(); 00558 from->setName( attendee->name() ); 00559 from->setEmail( attendee->email() ); 00560 } 00561 00562 if ( !d->mFreeBusyCache->saveFreeBusy( freebusy, from ) ) { 00563 return false; 00564 } 00565 00566 deleteTransaction( incidence ); 00567 return true; 00568 }