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() 00203 << " since I'm still NeedsAction there"; 00204 isUpdate = false; 00205 break; 00206 } 00207 } 00208 if ( isUpdate ) { 00209 if ( existingIncidence->revision() == inc->revision() && 00210 existingIncidence->lastModified() > inc->lastModified() ) { 00211 // This isn't an update - the found incidence was modified more recently 00212 kDebug() << "This isn't an update - the found incidence was modified more recently"; 00213 deleteTransaction( existingIncidence ); 00214 return false; 00215 } 00216 kDebug() << "replacing existing incidence " << existingIncidence->uid(); 00217 bool res = true; 00218 const QString oldUid = existingIncidence->uid(); 00219 if ( existingIncidence->type() != inc->type() ) { 00220 kError() << "assigning different incidence types"; 00221 res = false; 00222 } else { 00223 IncidenceBase *existingIncidenceBase = existingIncidence.data(); 00224 IncidenceBase *incBase = inc.data(); 00225 *existingIncidenceBase = *incBase; 00226 existingIncidence->setSchedulingID( inc->uid(), oldUid ); 00227 } 00228 deleteTransaction( incidence ); 00229 return res; 00230 } 00231 } else { 00232 // This isn't an update - the found incidence has a bigger revision number 00233 kDebug() << "This isn't an update - the found incidence has a bigger revision number"; 00234 deleteTransaction( incidence ); 00235 return false; 00236 } 00237 } 00238 00239 // Move the uid to be the schedulingID and make a unique UID 00240 inc->setSchedulingID( inc->uid(), CalFormat::createUniqueId() ); 00241 // notify the user in case this is an update and we didn't find the to-be-updated incidence 00242 if ( existingIncidences.count() == 0 && inc->revision() > 0 ) { 00243 KMessageBox::information( 00244 0, 00245 i18nc( "@info", 00246 "<para>You accepted an invitation update, but an earlier version of the " 00247 "item could not be found in your calendar.</para>" 00248 "<para>This may have occurred because:<list>" 00249 "<item>the organizer did not include you in the original invitation</item>" 00250 "<item>you did not accept the original invitation yet</item>" 00251 "<item>you deleted the original invitation from your calendar</item>" 00252 "<item>you no longer have access to the calendar containing the invitation</item>" 00253 "</list></para>" 00254 "<para>This is not a problem, but we thought you should know.</para>" ), 00255 i18nc( "@title", "Cannot find invitation to be updated" ), "AcceptCantFindIncidence" ); 00256 } 00257 kDebug() << "Storing new incidence with scheduling uid=" << inc->schedulingID() 00258 << " and uid=" << inc->uid(); 00259 mCalendar->addIncidence( inc ); 00260 00261 deleteTransaction( incidence ); 00262 return true; 00263 } 00264 00265 bool Scheduler::acceptAdd( const IncidenceBase::Ptr &incidence, 00266 ScheduleMessage::Status /* status */) 00267 { 00268 deleteTransaction( incidence ); 00269 return false; 00270 } 00271 00272 bool Scheduler::acceptCancel( const IncidenceBase::Ptr &incidence, 00273 ScheduleMessage::Status status, 00274 const QString &attendee ) 00275 { 00276 Incidence::Ptr inc = incidence.staticCast<Incidence>(); 00277 if ( !inc ) { 00278 return false; 00279 } 00280 00281 if ( inc->type() == IncidenceBase::TypeFreeBusy ) { 00282 // reply to this request is handled in korganizer's incomingdialog 00283 return true; 00284 } 00285 00286 const Incidence::List existingIncidences = mCalendar->incidencesFromSchedulingID( inc->uid() ); 00287 kDebug() << "Scheduler::acceptCancel=" 00288 << Stringify::scheduleMessageStatus( status ) //krazy2:exclude=kdebug 00289 << ": found " << existingIncidences.count() 00290 << " incidences with schedulingID " << inc->schedulingID(); 00291 00292 bool ret = false; 00293 Incidence::List::ConstIterator incit = existingIncidences.begin(); 00294 for ( ; incit != existingIncidences.end() ; ++incit ) { 00295 Incidence::Ptr i = *incit; 00296 kDebug() << "Considering this found event (" 00297 << ( i->isReadOnly() ? "readonly" : "readwrite" ) 00298 << ") :" << mFormat->toString( i ); 00299 00300 // If it's readonly, we can't possible remove it. 00301 if ( i->isReadOnly() ) { 00302 continue; 00303 } 00304 00305 // Code for new invitations: 00306 // We cannot check the value of "status" to be RequestNew because 00307 // "status" comes from a similar check inside libical, where the event 00308 // is compared to other events in the calendar. But if we have another 00309 // version of the event around (e.g. shared folder for a group), the 00310 // status could be RequestNew, Obsolete or Updated. 00311 kDebug() << "looking in " << i->uid() << "'s attendees"; 00312 00313 // This is supposed to be a new request, not an update - however we want 00314 // to update the existing one to handle the "clicking more than once 00315 // on the invitation" case. So check the attendee status of the attendee. 00316 bool isMine = true; 00317 const Attendee::List attendees = i->attendees(); 00318 Attendee::List::ConstIterator ait; 00319 for ( ait = attendees.begin(); ait != attendees.end(); ++ait ) { 00320 if ( (*ait)->email() == attendee && 00321 (*ait)->status() == Attendee::NeedsAction ) { 00322 // This incidence wasn't created by me - it's probably in a shared 00323 // folder and meant for someone else, ignore it. 00324 kDebug() << "ignoring " << i->uid() 00325 << " since I'm still NeedsAction there"; 00326 isMine = false; 00327 break; 00328 } 00329 } 00330 00331 if ( isMine ) { 00332 kDebug() << "removing existing incidence " << i->uid(); 00333 if ( i->type() == IncidenceBase::TypeEvent ) { 00334 Event::Ptr event = mCalendar->event( i->uid() ); 00335 ret = ( event && mCalendar->deleteEvent( event ) ); 00336 } else if ( i->type() == IncidenceBase::TypeTodo ) { 00337 Todo::Ptr todo = mCalendar->todo( i->uid() ); 00338 ret = ( todo && mCalendar->deleteTodo( todo ) ); 00339 } 00340 deleteTransaction( incidence ); 00341 return ret; 00342 } 00343 } 00344 00345 // in case we didn't find the to-be-removed incidence 00346 if ( existingIncidences.count() > 0 && inc->revision() > 0 ) { 00347 KMessageBox::error( 00348 0, 00349 i18nc( "@info", 00350 "The event or task could not be removed from your calendar. " 00351 "Maybe it has already been deleted or is not owned by you. " 00352 "Or it might belong to a read-only or disabled calendar." ) ); 00353 } 00354 deleteTransaction( incidence ); 00355 return ret; 00356 } 00357 00358 bool Scheduler::acceptDeclineCounter( const IncidenceBase::Ptr &incidence, 00359 ScheduleMessage::Status status ) 00360 { 00361 Q_UNUSED( status ); 00362 deleteTransaction( incidence ); 00363 return false; 00364 } 00365 00366 bool Scheduler::acceptReply( const IncidenceBase::Ptr &incidence, ScheduleMessage::Status status, 00367 iTIPMethod method ) 00368 { 00369 Q_UNUSED( status ); 00370 if ( incidence->type() == IncidenceBase::TypeFreeBusy ) { 00371 return acceptFreeBusy( incidence, method ); 00372 } 00373 bool ret = false; 00374 Event::Ptr ev = mCalendar->event( incidence->uid() ); 00375 Todo::Ptr to = mCalendar->todo( incidence->uid() ); 00376 00377 // try harder to find the correct incidence 00378 if ( !ev && !to ) { 00379 const Incidence::List list = mCalendar->incidences(); 00380 for ( Incidence::List::ConstIterator it=list.constBegin(), end=list.constEnd(); 00381 it != end; ++it ) { 00382 if ( (*it)->schedulingID() == incidence->uid() ) { 00383 ev = ( *it ).dynamicCast<Event>(); 00384 to = ( *it ).dynamicCast<Todo>(); 00385 break; 00386 } 00387 } 00388 } 00389 00390 if ( ev || to ) { 00391 //get matching attendee in calendar 00392 kDebug() << "match found!"; 00393 Attendee::List attendeesIn = incidence->attendees(); 00394 Attendee::List attendeesEv; 00395 Attendee::List attendeesNew; 00396 if ( ev ) { 00397 attendeesEv = ev->attendees(); 00398 } 00399 if ( to ) { 00400 attendeesEv = to->attendees(); 00401 } 00402 Attendee::List::ConstIterator inIt; 00403 Attendee::List::ConstIterator evIt; 00404 for ( inIt = attendeesIn.constBegin(); inIt != attendeesIn.constEnd(); ++inIt ) { 00405 Attendee::Ptr attIn = *inIt; 00406 bool found = false; 00407 for ( evIt = attendeesEv.constBegin(); evIt != attendeesEv.constEnd(); ++evIt ) { 00408 Attendee::Ptr attEv = *evIt; 00409 if ( attIn->email().toLower() == attEv->email().toLower() ) { 00410 //update attendee-info 00411 kDebug() << "update attendee"; 00412 attEv->setStatus( attIn->status() ); 00413 attEv->setDelegate( attIn->delegate() ); 00414 attEv->setDelegator( attIn->delegator() ); 00415 ret = true; 00416 found = true; 00417 } 00418 } 00419 if ( !found && attIn->status() != Attendee::Declined ) { 00420 attendeesNew.append( attIn ); 00421 } 00422 } 00423 00424 bool attendeeAdded = false; 00425 for ( Attendee::List::ConstIterator it = attendeesNew.constBegin(); 00426 it != attendeesNew.constEnd(); ++it ) { 00427 Attendee::Ptr attNew = *it; 00428 QString msg = 00429 i18nc( "@info", "%1 wants to attend %2 but was not invited.", 00430 attNew->fullName(), 00431 ( ev ? ev->summary() : to->summary() ) ); 00432 if ( !attNew->delegator().isEmpty() ) { 00433 msg = i18nc( "@info", "%1 wants to attend %2 on behalf of %3.", 00434 attNew->fullName(), 00435 ( ev ? ev->summary() : to->summary() ), attNew->delegator() ); 00436 } 00437 if ( KMessageBox::questionYesNo( 00438 0, msg, i18nc( "@title", "Uninvited attendee" ), 00439 KGuiItem( i18nc( "@option", "Accept Attendance" ) ), 00440 KGuiItem( i18nc( "@option", "Reject Attendance" ) ) ) != KMessageBox::Yes ) { 00441 Incidence::Ptr cancel = incidence.dynamicCast<Incidence>(); 00442 if ( cancel ) { 00443 cancel->addComment( 00444 i18nc( "@info", 00445 "The organizer rejected your attendance at this meeting." ) ); 00446 } 00447 performTransaction( incidence, iTIPCancel, attNew->fullName() ); 00448 // ### can't delete cancel here because it is aliased to incidence which 00449 // is accessed in the next loop iteration (CID 4232) 00450 // delete cancel; 00451 continue; 00452 } 00453 00454 Attendee::Ptr a( new Attendee( attNew->name(), attNew->email(), attNew->RSVP(), 00455 attNew->status(), attNew->role(), attNew->uid() ) ); 00456 00457 a->setDelegate( attNew->delegate() ); 00458 a->setDelegator( attNew->delegator() ); 00459 if ( ev ) { 00460 ev->addAttendee( a ); 00461 } else if ( to ) { 00462 to->addAttendee( a ); 00463 } 00464 ret = true; 00465 attendeeAdded = true; 00466 } 00467 00468 // send update about new participants 00469 if ( attendeeAdded ) { 00470 bool sendMail = false; 00471 if ( ev || to ) { 00472 if ( KMessageBox::questionYesNo( 00473 0, 00474 i18nc( "@info", 00475 "An attendee was added to the incidence. " 00476 "Do you want to email the attendees an update message?" ), 00477 i18nc( "@title", "Attendee Added" ), 00478 KGuiItem( i18nc( "@option", "Send Messages" ) ), 00479 KGuiItem( i18nc( "@option", "Do Not Send" ) ) ) == KMessageBox::Yes ) { 00480 sendMail = true; 00481 } 00482 } 00483 00484 if ( ev ) { 00485 ev->setRevision( ev->revision() + 1 ); 00486 if ( sendMail ) { 00487 performTransaction( ev, iTIPRequest ); 00488 } 00489 } 00490 if ( to ) { 00491 to->setRevision( to->revision() + 1 ); 00492 if ( sendMail ) { 00493 performTransaction( to, iTIPRequest ); 00494 } 00495 } 00496 } 00497 00498 if ( ret ) { 00499 // We set at least one of the attendees, so the incidence changed 00500 // Note: This should not result in a sequence number bump 00501 if ( ev ) { 00502 ev->updated(); 00503 } else if ( to ) { 00504 to->updated(); 00505 } 00506 } 00507 if ( to ) { 00508 // for VTODO a REPLY can be used to update the completion status of 00509 // a to-do. see RFC2446 3.4.3 00510 Todo::Ptr update = incidence.dynamicCast<Todo>(); 00511 Q_ASSERT( update ); 00512 if ( update && ( to->percentComplete() != update->percentComplete() ) ) { 00513 to->setPercentComplete( update->percentComplete() ); 00514 to->updated(); 00515 } 00516 } 00517 } else { 00518 kError() << "No incidence for scheduling."; 00519 } 00520 00521 if ( ret ) { 00522 deleteTransaction( incidence ); 00523 } 00524 return ret; 00525 } 00526 00527 bool Scheduler::acceptRefresh( const IncidenceBase::Ptr &incidence, ScheduleMessage::Status status ) 00528 { 00529 Q_UNUSED( status ); 00530 // handled in korganizer's IncomingDialog 00531 deleteTransaction( incidence ); 00532 return false; 00533 } 00534 00535 bool Scheduler::acceptCounter( const IncidenceBase::Ptr &incidence, ScheduleMessage::Status status ) 00536 { 00537 Q_UNUSED( status ); 00538 deleteTransaction( incidence ); 00539 return false; 00540 } 00541 00542 bool Scheduler::acceptFreeBusy( const IncidenceBase::Ptr &incidence, iTIPMethod method ) 00543 { 00544 if ( !d->mFreeBusyCache ) { 00545 kError() << "Scheduler: no FreeBusyCache."; 00546 return false; 00547 } 00548 00549 FreeBusy::Ptr freebusy = incidence.staticCast<FreeBusy>(); 00550 00551 kDebug() << "freeBusyDirName:" << freeBusyDir(); 00552 00553 Person::Ptr from; 00554 if( method == iTIPPublish ) { 00555 from = freebusy->organizer(); 00556 } 00557 if ( ( method == iTIPReply ) && ( freebusy->attendeeCount() == 1 ) ) { 00558 Attendee::Ptr attendee = freebusy->attendees().first(); 00559 from->setName( attendee->name() ); 00560 from->setEmail( attendee->email() ); 00561 } 00562 00563 if ( !d->mFreeBusyCache->saveFreeBusy( freebusy, from ) ) { 00564 return false; 00565 } 00566 00567 deleteTransaction( incidence ); 00568 return true; 00569 }