0010046: config for disabling external imip
[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-2012 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 && $_iMIP->getEvent()->isObsoletedBy($existingEvent)) {
194             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_RECENT, "old iMIP message");
195             $result = FALSE;
196         }
197         
198         return $result;
199     }
200     
201     /**
202     * returns and optionally asserts own attendee record
203     *
204     * @param  Calendar_Model_iMIP   $_iMIP
205     * @param  boolean               $_assertExistence
206     * @param  boolean               $_assertOriginator
207     * @return boolean
208     */
209     protected function _assertOwnAttender($_iMIP, $_assertExistence, $_assertOriginator)
210     {
211         $result = TRUE;
212         
213         $existingEvent = $_iMIP->getExistingEvent();
214         $ownAttender = Calendar_Model_Attender::getOwnAttender($existingEvent ? $existingEvent->attendee : $_iMIP->getEvent()->attendee);
215         if ($_assertExistence && ! $ownAttender) {
216             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ATTENDEE, "processing {$_iMIP->method} for non attendee is not supported");
217             $result = FALSE;
218         }
219         
220         if ($_assertOriginator) {
221             $result &= $this->_assertOriginator($_iMIP, $ownAttender->getResolvedUser(), 'own attendee');
222         }
223         
224         return $result;
225     }
226     
227     /**
228      * assert originator
229      * 
230      * @param Calendar_Model_iMIP $_iMIP
231      * @param Addressbook_Model_Contact $_contact
232      * @param string $_who
233      * @return boolean
234      */
235     protected function _assertOriginator(Calendar_Model_iMIP $_iMIP, $_contact, $_who)
236     {
237         if ($_contact === NULL) {
238             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORIGINATOR, $_who . " could not be found.");
239             return FALSE;
240         }
241         
242         $contactEmails = array($_contact->email, $_contact->email_home);
243         if(! in_array($_iMIP->originator, $contactEmails)) {
244             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__
245             . ' originator ' . $_iMIP->originator . ' ! in_array() '. print_r($contactEmails, TRUE));
246         
247             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORIGINATOR, $_who . " must be the same as originator of iMIP -> spoofing attempt?");
248             return FALSE;
249         } else {
250             return TRUE;
251         }
252     }
253     
254     /**
255     * returns and optionally asserts own attendee record
256     *
257     * @param  Calendar_Model_iMIP   $_iMIP
258     * @param  bool                  $_assertExistence
259     * @param  bool                  $_assertOriginator
260     * @param  bool                  $_assertAccount
261     * @return Addressbook_Model_Contact
262     * @throws Calendar_Exception_iMIP
263     * 
264     * @todo this needs to be splitted into assertExternalOrganizer / assertInternalOrganizer
265     */
266     protected function _assertOrganizer($_iMIP, $_assertExistence, $_assertOriginator, $_assertAccount = false)
267     {
268         $result = TRUE;
269         
270         $existingEvent = $_iMIP->getExistingEvent();
271         $organizer = $existingEvent ? $existingEvent->resolveOrganizer() : $_iMIP->getEvent()->resolveOrganizer();
272         
273         if ($_assertExistence && ! $organizer) {
274             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORGANIZER, "processing {$_iMIP->method} without organizer is not possible");
275             $result = FALSE;
276         }
277         
278         // NOTE: originator might also be reply-to instead of from
279         // NOTE: originator might act on behalf of organizer ("SENT-BY    ")
280         // NOTE: an existing event might be updateable by an non organizer ("SENT-BY    ") originator
281         // NOTE: CUA might skip the SENT-BY     param => bad luck
282         /*
283         if ($_assertOriginator) {
284             $result &= $this->_assertOriginator($_iMIP, $organizer, 'organizer');
285         }
286         */
287         
288         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
289             . ' Organizer: ' . ($organizer ? print_r($organizer->toArray(), true) : 'not found'));
290         
291         // config setting overwrites method param
292         $assertAccount = Calendar_Config::getInstance()->get(Calendar_Config::DISABLE_EXTERNAL_IMIP, $_assertAccount);
293         if ($assertAccount && (! $organizer || ! $organizer->account_id)) {
294             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORGANIZER, "processing {$_iMIP->method} without organizer user account is not possible");
295             $result = FALSE;
296         }
297         
298         return $result;
299     }
300     
301     /**
302      * process request
303      * 
304      * @param  Calendar_Model_iMIP   $_iMIP
305      * @param  string                $_status
306      * @throws Tinebase_Exception_NotImplemented
307      * 
308      * @todo handle external organizers
309      * @todo create event in the organizers context
310      */
311     protected function _processRequest($_iMIP, $_status)
312     {
313         $existingEvent = $_iMIP->getExistingEvent();
314         $ownAttender = Calendar_Model_Attender::getOwnAttender($existingEvent ? $existingEvent->attendee : $_iMIP->getEvent()->attendee);
315         $organizer = $existingEvent ? $existingEvent->resolveOrganizer() : $_iMIP->getEvent()->resolveOrganizer();
316         
317         // internal organizer:
318         //  - event is up to date
319         //  - status change could also be done by calendar method
320         //  - normal notifications
321         if ($organizer->account_id) {
322             if (! $existingEvent) {
323                 // organizer has an account but no event exists, it seems that event was created from a non-caldav client
324                 // do not send notifications in this case + create event in context of organizer
325                 return; // not clear how to create in the organizers context...
326                 $sendNotifications = Calendar_Controller_Event::getInstance()->sendNotifications(FALSE);
327                 $existingEvent = Calendar_Controller_MSEventFacade::getInstance()->create($_iMIP->getEvent());
328                 Calendar_Controller_Event::getInstance()->sendNotifications($sendNotifications);
329             }
330             
331             if ($_status && $_status != $ownAttender->status) {
332                 $ownAttender->status = $_status;
333                 Calendar_Controller_Event::getInstance()->attenderStatusUpdate($existingEvent, $ownAttender, $ownAttender->status_authkey);
334             }
335         }
336         
337         // external organizer:
338         else {
339             if ($ownAttender && $_status) {
340                 $ownAttender->status = $_status;
341             }
342             
343             if (! $existingEvent) {
344                 $event = $_iMIP->getEvent();
345                 if (! $event->container_id) {
346                     $event->container_id = Tinebase_Core::getPreference('Calendar')->{Calendar_Preference::DEFAULTCALENDAR};
347                 }
348                 
349                 $_iMIP->event = Calendar_Controller_MSEventFacade::getInstance()->create($event);
350             } else {
351                 $_iMIP->event = Calendar_Controller_MSEventFacade::getInstance()->update($existingEvent);
352             }
353             
354             //  - send reply to organizer
355         }
356     }
357     
358     /**
359     * reply precondition
360     *
361     * @TODO an internal reply should trigge a RECENT precondition
362     * @TODO distinguish RECENT and PROCESSED preconditions?
363     * 
364     * @param  Calendar_Model_iMIP   $_iMIP
365     * @return boolean
366     */
367     protected function _checkReplyPreconditions($_iMIP)
368     {
369         $result = TRUE;
370         
371         $existingEvent = $_iMIP->getExistingEvent();
372         if (! $existingEvent) {
373             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_EVENTEXISTS, "cannot process REPLY to non existent/invisible event");
374             $result = FALSE;
375         }
376         
377         $iMIPAttenderIdx = $_iMIP->getEvent()->attendee instanceof Tinebase_Record_RecordSet ? array_search($_iMIP->originator, $_iMIP->getEvent()->attendee->getEmail()) : FALSE;
378         $iMIPAttender = $iMIPAttenderIdx !== FALSE ? $_iMIP->getEvent()->attendee[$iMIPAttenderIdx] : NULL;
379         $iMIPAttenderStatus = $iMIPAttender ? $iMIPAttender->status : NULL;
380         $eventAttenderIdx = $existingEvent->attendee instanceof Tinebase_Record_RecordSet ? array_search($_iMIP->originator, $existingEvent->attendee->getEmail()) : FALSE;
381         $eventAttender = $eventAttenderIdx !== FALSE ? $existingEvent->attendee[$eventAttenderIdx] : NULL;
382         $eventAttenderStatus = $eventAttender ? $eventAttender->status : NULL;
383         
384         if ($_iMIP->getEvent()->isObsoletedBy($existingEvent)) {
385             
386             // allow non RECENT replies if no reschedule and STATUS_NEEDSACTION
387             if ($eventAttenderStatus != Calendar_Model_Attender::STATUS_NEEDSACTION || $existingEvent->isRescheduled($_iMIP->getEvent())) {
388                 $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_RECENT, "old iMIP message");
389                 $result = FALSE;
390             }
391         }
392         
393         if (! is_null($iMIPAttenderStatus) && $iMIPAttenderStatus == $eventAttenderStatus) {
394             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_TOPROCESS, "this REPLY was already processed");
395             $result = FALSE;
396         }
397         
398         if (! $eventAttender) {
399             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORIGINATOR, "originator is not attendee in existing event -> party crusher?");
400             $result = FALSE;
401         }
402         
403         if (! $iMIPAttender) {
404             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ORIGINATOR, "originator is not attendee in iMIP transaction -> spoofing attempt?");
405             $result = FALSE;
406         }
407         
408         // TODO fix organizer account asserting
409         if (! $this->_assertOrganizer($_iMIP, TRUE, FALSE/*, $_assertAccount = TRUE */)) {
410             $result = FALSE;
411         }
412         
413         return $result;
414     }
415     
416     /**
417      * process reply
418      * 
419      * some attender replied to my request (I'm Organizer) -> update status (seq++) / send notifications!
420      * 
421      * NOTE: only external replies should be processed here
422      *       @todo check silence for internal replies
423      *       
424      * @param  Calendar_Model_iMIP   $_iMIP
425      */
426     protected function _processReply(Calendar_Model_iMIP $_iMIP)
427     {
428         // merge ics into existing event
429         $existingEvent = $_iMIP->getExistingEvent();
430         $event = $_iMIP->mergeEvent($existingEvent);
431         $attendee = $event->attendee[array_search($_iMIP->originator, $existingEvent->attendee->getEmail())];
432         
433         // NOTE: if current user has no rights to the calendar, status update is not applied
434         Calendar_Controller_MSEventFacade::getInstance()->attenderStatusUpdate($event, $attendee);
435     }
436     
437     /**
438     * add precondition
439     *
440     * @param  Calendar_Model_iMIP   $_iMIP
441     * @return boolean
442     *
443     * @todo implement
444     */
445     protected function _checkAddPreconditions($_iMIP)
446     {
447         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing add requests is not supported yet');
448     
449         return FALSE;
450     }
451     
452     /**
453     * process add
454     *
455     * @param  Calendar_Model_iMIP   $_iMIP
456     * 
457     * @todo implement
458     */
459     protected function _processAdd($_iMIP)
460     {
461         // organizer added a meeting/recurrance to an existing event -> update event
462         // internal organizer:
463         //  - event is up to date nothing to do
464         // external organizer:
465         //  - update event
466         //  - the iMIP is already the notification mail!
467     }
468     
469     /**
470     * cancel precondition
471     *
472     * @param  Calendar_Model_iMIP   $_iMIP
473     * @return boolean
474     *
475     * @todo implement
476     */
477     protected function _checkCancelPreconditions($_iMIP)
478     {
479         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing CANCEL is not supported yet');
480     
481         return FALSE;
482     }
483     
484     /**
485     * process cancel
486     *
487     * @param  Calendar_Model_iMIP   $_iMIP
488     * @param  Calendar_Model_Event  $_existingEvent
489     * 
490     * @todo implement
491     */
492     protected function _processCancel($_iMIP, $_existingEvent)
493     {
494         // organizer cancelled meeting/recurrence of an existing event -> update event
495         // the iMIP is already the notification mail!
496     }
497     
498     /**
499     * refresh precondition
500     *
501     * @param  Calendar_Model_iMIP   $_iMIP
502     * @return boolean
503     *
504     * @todo implement
505     */
506     protected function _checkRefreshPreconditions($_iMIP)
507     {
508         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing REFRESH is not supported yet');
509     
510         return FALSE;
511     }
512     
513     /**
514     * process refresh
515     *
516     * @param  Calendar_Model_iMIP   $_iMIP
517     *
518     * @todo implement
519     */
520     protected function _processRefresh($_iMIP)
521     {
522         // always internal organizer
523         //  - send message
524         //  - mark iMIP message ANSWERED
525     }
526     
527     /**
528     * counter precondition
529     *
530     * @param  Calendar_Model_iMIP   $_iMIP
531     * @return boolean
532     *
533     * @todo implement
534     */
535     protected function _checkCounterPreconditions($_iMIP)
536     {
537         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing COUNTER is not supported yet');
538     
539         return FALSE;
540     }
541     
542     /**
543     * process counter
544     *
545     * @param  Calendar_Model_iMIP   $_iMIP
546     *
547     * @todo implement
548     */
549     protected function _processCounter($_iMIP)
550     {
551         // some attendee suggests to change the event
552         // status: ACCEPT => update event, send notifications to all
553         // status: DECLINE => send DECLINECOUNTER to originator
554         // mark message ANSWERED
555     }
556     
557     /**
558     * declinecounter precondition
559     *
560     * @param  Calendar_Model_iMIP   $_iMIP
561     * @return boolean
562     *
563     * @todo implement
564     */
565     protected function _checkDeclinecounterPreconditions($_iMIP)
566     {
567         $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_SUPPORTED, 'processing DECLINECOUNTER is not supported yet');
568     
569         return FALSE;
570     }
571     
572     /**
573     * process declinecounter
574     *
575     * @param  Calendar_Model_iMIP   $_iMIP
576     *
577     * @todo implement
578     */
579     protected function _processDeclinecounter($_iMIP)
580     {
581         // organizer declined my counter request of an existing event -> update event
582     }
583 }