b9995db807b7e1b5b72505ccfa624925dab280f3
[tine20] / tine20 / Calendar / Frontend / iMIP.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Calendar
6  * @subpackage  Frontend
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Cornelius Weiss <c.weiss@metaways.de>
9  * @copyright   Copyright (c) 2011-2014 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * iMIP (RFC 6047) frontend for calendar
14  * 
15  * @package     Calendar
16  * @subpackage  Frontend
17  */
18 class Calendar_Frontend_iMIP
19 {
20     /**
21      * auto process given iMIP component 
22      * 
23      * @TODO autodelete REFRESH mails
24      * 
25      * @param  Calendar_Model_iMIP $_iMIP
26      */
27     public function autoProcess($_iMIP)
28     {
29         if ($_iMIP->method == Calendar_Model_iMIP::METHOD_COUNTER) {
30             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . " skip auto processing of iMIP component with COUNTER method -> must always be processed manually");
31             return;
32         }
33         
34         if (! $_iMIP->getExistingEvent(TRUE)) {
35             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . " skip auto processing of iMIP component whose event is not in our db yet");
36             return;
37         }
38         
39         // update existing event details _WITHOUT_ status updates
40         return $this->_process($_iMIP);
41     }
42     
43     /**
44      * manual process iMIP component and optionally set status
45      * 
46      * @param  Calendar_Model_iMIP   $_iMIP
47      * @param  string                $_status
48      */
49     public function process($_iMIP, $_status = NULL)
50     {
51         // client spoofing protection
52         $iMIP = Felamimail_Controller_Message::getInstance()->getiMIP($_iMIP->getId());
53         
54         return $this->_process($_iMIP, $_status);
55     }
56     
57     /**
58      * prepares iMIP component for client
59      *  
60      * @param Calendar_Model_iMIP $_iMIP
61      * @param boolean $_throwException
62      * @return Calendar_Model_iMIP
63      */
64     public function prepareComponent($_iMIP, $_throwException = false)
65     {
66         $this->_checkPreconditions($_iMIP, $_throwException);
67         
68         Calendar_Convert_Event_Json::resolveRelatedData($_iMIP->event);
69         Tinebase_Model_Container::resolveContainerOfRecord($_iMIP->event);
70         Tinebase_Model_Container::resolveContainerOfRecord($_iMIP->getExistingEvent());
71         
72         return $_iMIP;
73     }
74     
75     /**
76      * check precondtions
77      * 
78      * @param Calendar_Model_iMIP $_iMIP
79      * @param boolean $_throwException
80      * @param string $_status
81      * @throws Calendar_Exception_iMIP
82      * @return boolean
83      * 
84      * @todo add iMIP record to exception when it extends the Data exception
85      */
86     protected function _checkPreconditions(Calendar_Model_iMIP $_iMIP, $_throwException = FALSE, $_status = NULL)
87     {
88         if ($_iMIP->preconditionsChecked) {
89             if (empty($_iMIP->preconditions) || ! $_throwException) {
90                 return;
91             } else {
92                 throw new Calendar_Exception_iMIP('iMIP preconditions failed: ' . implode(', ', array_keys($_iMIP->preconditions)));
93             }
94         }
95         
96         $method = $_iMIP->method ? ucfirst(strtolower($_iMIP->method)) : 'MISSINGMETHOD';
97         
98         $preconditionMethodName  = '_check'     . $method . 'Preconditions';
99         if (method_exists($this, $preconditionMethodName)) {
100             $preconditionCheckSuccessful = $this->{$preconditionMethodName}($_iMIP, $_status);
101         } else {
102             $preconditionCheckSuccessful = TRUE;
103             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . " No preconditions check fn found for method " . $method);
104         }
105         
106         $_iMIP->preconditionsChecked = TRUE;
107         
108         if ($_throwException && ! $preconditionCheckSuccessful) {
109             throw new Calendar_Exception_iMIP('iMIP preconditions failed: ' . implode(', ', array_keys($_iMIP->preconditions)));
110         }
111         
112         return $preconditionCheckSuccessful;
113     }
114     
115     /**
116      * assemble an iMIP component in the notification flow
117      * 
118      * @todo implement
119      */
120     public function assembleComponent()
121     {
122         // cancel normal vs. recur instance
123     }
124     
125     /**
126      * process iMIP component and optionally set status
127      * 
128      * @param  Calendar_Model_iMIP   $_iMIP
129      * @param  string                $_status
130      * @return mixed
131      */
132     protected function _process($_iMIP, $_status = NULL)
133     {
134         $method                  = ucfirst(strtolower($_iMIP->method));
135         $processMethodName       = '_process'   . $method;
136         
137         if (! method_exists($this, $processMethodName)) {
138             throw new Tinebase_Exception_UnexpectedValue("Method {$_iMIP->method} not supported");
139         }
140         
141         $this->_checkPreconditions($_iMIP, TRUE, $_status);
142         $result = $this->{$processMethodName}($_iMIP, $_status);
143         
144         //clear existing event cache
145         unset($_iMIP->existing_event);
146         
147         return $result;
148     }
149     
150     /**
151      * publish precondition
152      * 
153      * @param  Calendar_Model_iMIP   $_iMIP
154      * @return boolean
155      * 
156      * @todo implement
157      */
158     protected function _checkPublishPreconditions($_iMIP)
159     {
160         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing published events is not supported yet');
161         
162         return FALSE;
163     }
164     
165     /**
166      * process publish
167      * 
168      * @param  Calendar_Model_iMIP   $_iMIP
169      * 
170      * @todo implement
171      */
172     protected function _processPublish($_iMIP)
173     {
174         // add/update event (if outdated) / no status stuff / DANGER of duplicate UIDs
175         // -  no notifications!
176     }
177     
178     /**
179      * request precondition
180      * 
181      * @param  Calendar_Model_iMIP   $_iMIP
182      * @return boolean
183      */
184     protected function _checkRequestPreconditions($_iMIP)
185     {
186         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
187             . ' Checking REQUEST preconditions of iMIP ...');
188         
189         $result  = $this->_assertOwnAttender($_iMIP, TRUE, FALSE);
190         $result &= $this->_assertOrganizer($_iMIP, TRUE, TRUE);
191         
192         $existingEvent = $_iMIP->getExistingEvent();
193         if ($existingEvent) {
194             $iMIPEvent = $_iMIP->getEvent();
195             $isObsoleted = false;
196             
197             if (! $existingEvent->hasExternalOrganizer() && $iMIPEvent->isObsoletedBy($existingEvent)) {
198                 $isObsoleted = true;
199             }
200             
201             else if ($iMIPEvent->external_seq < $existingEvent->external_seq) {
202                 $isObsoleted = true;
203             }
204             
205             // allow if not rescheduled
206             if ($isObsoleted && $existingEvent->isRescheduled($iMIPEvent)) {
207                 $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_RECENT, "old iMIP message");
208                 $result = FALSE;
209             }
210         }
211         
212         return $result;
213     }
214     
215     /**
216     * returns and optionally asserts own attendee record
217     *
218     * @param  Calendar_Model_iMIP   $_iMIP
219     * @param  boolean               $_assertExistence
220     * @param  boolean               $_assertOriginator
221     * @return boolean
222     */
223     protected function _assertOwnAttender($_iMIP, $_assertExistence, $_assertOriginator)
224     {
225         $result = TRUE;
226         
227         $existingEvent = $_iMIP->getExistingEvent();
228         $ownAttender = Calendar_Model_Attender::getOwnAttender($existingEvent ? $existingEvent->attendee : $_iMIP->getEvent()->attendee);
229         if ($_assertExistence && ! $ownAttender) {
230             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ATTENDEE, "processing {$_iMIP->method} for non attendee is not supported");
231             $result = FALSE;
232         }
233         
234         if ($_assertOriginator) {
235             $result &= $this->_assertOriginator($_iMIP, $ownAttender->getResolvedUser(), 'own attendee');
236         }
237         
238         return $result;
239     }
240     
241     /**
242      * assert originator
243      * 
244      * @param Calendar_Model_iMIP $_iMIP
245      * @param Addressbook_Model_Contact $_contact
246      * @param string $_who
247      * @return boolean
248      */
249     protected function _assertOriginator(Calendar_Model_iMIP $_iMIP, $_contact, $_who)
250     {
251         if ($_contact === NULL) {
252             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORIGINATOR, $_who . " could not be found.");
253             return FALSE;
254         }
255         
256         $contactEmails = array($_contact->email, $_contact->email_home);
257         if(! in_array($_iMIP->originator, $contactEmails)) {
258             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__
259             . ' originator ' . $_iMIP->originator . ' ! in_array() '. print_r($contactEmails, TRUE));
260         
261             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORIGINATOR, $_who . " must be the same as originator of iMIP -> spoofing attempt?");
262             return FALSE;
263         } else {
264             return TRUE;
265         }
266     }
267     
268     /**
269      * 
270      *
271      * @param  Calendar_Model_iMIP   $_iMIP
272      * @param  bool                  $_assertExistence
273      * @param  bool                  $_assertOriginator
274      * @param  bool                  $_assertAccount
275      * @return Addressbook_Model_Contact
276      * @throws Calendar_Exception_iMIP
277      * 
278      * @todo this needs to be splitted into assertExternalOrganizer / assertInternalOrganizer
279      */
280     protected function _assertOrganizer($_iMIP, $_assertExistence, $_assertOriginator, $_assertAccount = false)
281     {
282         $result = TRUE;
283         
284         $existingEvent = $_iMIP->getExistingEvent();
285         $organizer = $existingEvent ? $existingEvent->resolveOrganizer() : $_iMIP->getEvent()->resolveOrganizer();
286         
287         if ($_assertExistence && ! $organizer) {
288             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORGANIZER, "processing {$_iMIP->method} without organizer is not possible");
289             $result = FALSE;
290         }
291         
292         // NOTE: originator might also be reply-to instead of from
293         // NOTE: originator might act on behalf of organizer ("SENT-BY    ")
294         // NOTE: an existing event might be updateable by an non organizer ("SENT-BY    ") originator
295         // NOTE: CUA might skip the SENT-BY     param => bad luck
296         /*
297         if ($_assertOriginator) {
298             $result &= $this->_assertOriginator($_iMIP, $organizer, 'organizer');
299         }
300         */
301         
302         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
303             . ' Organizer: ' . ($organizer ? print_r($organizer->toArray(), true) : 'not found'));
304         
305         // config setting overwrites method param
306         $assertAccount = Calendar_Config::getInstance()->get(Calendar_Config::DISABLE_EXTERNAL_IMIP, $_assertAccount);
307         if ($assertAccount && (! $organizer || ! $organizer->account_id)) {
308             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORGANIZER, "processing {$_iMIP->method} without organizer user account is not possible");
309             $result = FALSE;
310         }
311         
312         return $result;
313     }
314     
315     /**
316      * process request
317      * 
318      * @param  Calendar_Model_iMIP   $_iMIP
319      * @param  string                $_status
320      */
321     protected function _processRequest($_iMIP, $_status)
322     {
323         $existingEvent = $_iMIP->getExistingEvent();
324         $ownAttender = Calendar_Model_Attender::getOwnAttender($existingEvent ? $existingEvent->attendee : $_iMIP->getEvent()->attendee);
325         $organizer = $existingEvent ? $existingEvent->resolveOrganizer() : $_iMIP->getEvent()->resolveOrganizer();
326         
327         // internal organizer:
328         //  - event is up to date
329         //  - status change could also be done by calendar method
330         //  - normal notifications
331         if ($organizer->account_id) {
332             if (! $existingEvent) {
333                 // organizer has an account but no event exists, it seems that event was created from a non-caldav client
334                 // do not send notifications in this case + create event in context of organizer
335                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
336                         . ' Organizer has an account but no event exists!');
337                 return; // not clear how to create in the organizers context...
338                 $sendNotifications = Calendar_Controller_Event::getInstance()->sendNotifications(FALSE);
339                 $existingEvent = Calendar_Controller_MSEventFacade::getInstance()->create($_iMIP->getEvent());
340                 Calendar_Controller_Event::getInstance()->sendNotifications($sendNotifications);
341             }
342             
343             if ($_status && $_status != $ownAttender->status) {
344                 $ownAttender->status = $_status;
345                 Calendar_Controller_Event::getInstance()->attenderStatusUpdate($existingEvent, $ownAttender, $ownAttender->status_authkey);
346             }
347         }
348         
349         // external organizer:
350         else {
351             $sendNotifications = Calendar_Controller_Event::getInstance()->sendNotifications(false);
352             if (! $existingEvent) {
353                 $event = $_iMIP->getEvent();
354                 if (! $event->container_id) {
355                     $event->container_id = Tinebase_Core::getPreference('Calendar')->{Calendar_Preference::DEFAULTCALENDAR};
356                 }
357                 
358                 $event = $_iMIP->event = Calendar_Controller_MSEventFacade::getInstance()->create($event);
359             } else {
360                 $event = $_iMIP->event = Calendar_Controller_MSEventFacade::getInstance()->update($existingEvent);
361             }
362             
363             Calendar_Controller_Event::getInstance()->sendNotifications($sendNotifications);
364             
365             $ownAttender = Calendar_Model_Attender::getOwnAttender($event->attendee);
366             
367             // NOTE: we do the status update in a separate call to trigger the right notifications
368             if ($ownAttender && $_status) {
369                 $ownAttender->status = $_status;
370                 $a = Calendar_Controller_Event::getInstance()->attenderStatusUpdate($event, $ownAttender, $ownAttender->status_authkey);
371             }
372         }
373     }
374     
375     /**
376      * reply precondition
377      *
378      * @TODO an internal reply should trigger a RECENT precondition
379      * @TODO distinguish RECENT and PROCESSED preconditions?
380      * 
381      * @param  Calendar_Model_iMIP   $_iMIP
382      * @return boolean
383      */
384     protected function _checkReplyPreconditions($_iMIP)
385     {
386         $result = TRUE;
387         
388         $existingEvent = $_iMIP->getExistingEvent();
389         if (! $existingEvent) {
390             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_EVENTEXISTS, "cannot process REPLY to non existent/invisible event");
391             $result = FALSE;
392         }
393         
394         $iMIPAttenderIdx = $_iMIP->getEvent()->attendee instanceof Tinebase_Record_RecordSet ? array_search($_iMIP->originator, $_iMIP->getEvent()->attendee->getEmail()) : FALSE;
395         $iMIPAttender = $iMIPAttenderIdx !== FALSE ? $_iMIP->getEvent()->attendee[$iMIPAttenderIdx] : NULL;
396         $iMIPAttenderStatus = $iMIPAttender ? $iMIPAttender->status : NULL;
397         $eventAttenderIdx = $existingEvent->attendee instanceof Tinebase_Record_RecordSet ? array_search($_iMIP->originator, $existingEvent->attendee->getEmail()) : FALSE;
398         $eventAttender = $eventAttenderIdx !== FALSE ? $existingEvent->attendee[$eventAttenderIdx] : NULL;
399         $eventAttenderStatus = $eventAttender ? $eventAttender->status : NULL;
400         
401         if ($_iMIP->getEvent()->isObsoletedBy($existingEvent)) {
402             
403             // allow non RECENT replies if no reschedule and STATUS_NEEDSACTION
404             if ($eventAttenderStatus != Calendar_Model_Attender::STATUS_NEEDSACTION || $existingEvent->isRescheduled($_iMIP->getEvent())) {
405                 $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_RECENT, "old iMIP message");
406                 $result = FALSE;
407             }
408         }
409         
410         if (! is_null($iMIPAttenderStatus) && $iMIPAttenderStatus == $eventAttenderStatus) {
411             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_TOPROCESS, "this REPLY was already processed");
412             $result = FALSE;
413         }
414         
415         if (! $eventAttender) {
416             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORIGINATOR, "originator is not attendee in existing event -> party crusher?");
417             $result = FALSE;
418         }
419         
420         if (! $iMIPAttender) {
421             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORIGINATOR, "originator is not attendee in iMIP transaction -> spoofing attempt?");
422             $result = FALSE;
423         }
424         
425         // TODO fix organizer account asserting
426         if (! $this->_assertOrganizer($_iMIP, TRUE, FALSE/*, $_assertAccount = TRUE */)) {
427             $result = FALSE;
428         }
429         
430         return $result;
431     }
432     
433     /**
434      * process reply
435      * 
436      * some attender replied to my request (I'm Organizer) -> update status (seq++) / send notifications!
437      * 
438      * NOTE: only external replies should be processed here
439      *       @todo check silence for internal replies
440      *       
441      * @param  Calendar_Model_iMIP   $_iMIP
442      */
443     protected function _processReply(Calendar_Model_iMIP $_iMIP)
444     {
445         // merge ics into existing event
446         $existingEvent = $_iMIP->getExistingEvent();
447         $event = $_iMIP->mergeEvent($existingEvent);
448         $attendee = $event->attendee[array_search($_iMIP->originator, $existingEvent->attendee->getEmail())];
449         
450         // NOTE: if current user has no rights to the calendar, status update is not applied
451         Calendar_Controller_MSEventFacade::getInstance()->attenderStatusUpdate($event, $attendee);
452     }
453     
454     /**
455     * add precondition
456     *
457     * @param  Calendar_Model_iMIP   $_iMIP
458     * @return boolean
459     *
460     * @todo implement
461     */
462     protected function _checkAddPreconditions($_iMIP)
463     {
464         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing add requests is not supported yet');
465     
466         return FALSE;
467     }
468     
469     /**
470     * process add
471     *
472     * @param  Calendar_Model_iMIP   $_iMIP
473     * 
474     * @todo implement
475     */
476     protected function _processAdd($_iMIP)
477     {
478         // organizer added a meeting/recurrance to an existing event -> update event
479         // internal organizer:
480         //  - event is up to date nothing to do
481         // external organizer:
482         //  - update event
483         //  - the iMIP is already the notification mail!
484     }
485     
486     /**
487     * cancel precondition
488     *
489     * @param  Calendar_Model_iMIP   $_iMIP
490     * @return boolean
491     *
492     * @todo implement
493     */
494     protected function _checkCancelPreconditions($_iMIP)
495     {
496         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing CANCEL is not supported yet');
497     
498         return FALSE;
499     }
500     
501     /**
502     * process cancel
503     *
504     * @param  Calendar_Model_iMIP   $_iMIP
505     * @param  Calendar_Model_Event  $_existingEvent
506     * 
507     * @todo implement
508     */
509     protected function _processCancel($_iMIP, $_existingEvent)
510     {
511         // organizer cancelled meeting/recurrence of an existing event -> update event
512         // the iMIP is already the notification mail!
513     }
514     
515     /**
516     * refresh precondition
517     *
518     * @param  Calendar_Model_iMIP   $_iMIP
519     * @return boolean
520     *
521     * @todo implement
522     */
523     protected function _checkRefreshPreconditions($_iMIP)
524     {
525         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing REFRESH is not supported yet');
526     
527         return FALSE;
528     }
529     
530     /**
531     * process refresh
532     *
533     * @param  Calendar_Model_iMIP   $_iMIP
534     *
535     * @todo implement
536     */
537     protected function _processRefresh($_iMIP)
538     {
539         // always internal organizer
540         //  - send message
541         //  - mark iMIP message ANSWERED
542     }
543     
544     /**
545     * counter precondition
546     *
547     * @param  Calendar_Model_iMIP   $_iMIP
548     * @return boolean
549     *
550     * @todo implement
551     */
552     protected function _checkCounterPreconditions($_iMIP)
553     {
554         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing COUNTER is not supported yet');
555     
556         return FALSE;
557     }
558     
559     /**
560     * process counter
561     *
562     * @param  Calendar_Model_iMIP   $_iMIP
563     *
564     * @todo implement
565     */
566     protected function _processCounter($_iMIP)
567     {
568         // some attendee suggests to change the event
569         // status: ACCEPT => update event, send notifications to all
570         // status: DECLINE => send DECLINECOUNTER to originator
571         // mark message ANSWERED
572     }
573     
574     /**
575     * declinecounter precondition
576     *
577     * @param  Calendar_Model_iMIP   $_iMIP
578     * @return boolean
579     *
580     * @todo implement
581     */
582     protected function _checkDeclinecounterPreconditions($_iMIP)
583     {
584         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing DECLINECOUNTER is not supported yet');
585     
586         return FALSE;
587     }
588     
589     /**
590     * process declinecounter
591     *
592     * @param  Calendar_Model_iMIP   $_iMIP
593     *
594     * @todo implement
595     */
596     protected function _processDeclinecounter($_iMIP)
597     {
598         // organizer declined my counter request of an existing event -> update event
599     }
600 }