Merge branch 'pu/2013.10-caldav' into 2014.09
[tine20] / tine20 / Calendar / Model / Attender.php
1 <?php
2 /**
3  * @package     Calendar
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
7  */
8
9 /**
10  * Model of an attendee
11  *
12  * @package Calendar
13  * @property Tinebase_DateTime alarm_ack_time
14  * @property Tinebase_DateTime alarm_snooze_time
15  * @property string transp
16  */
17 class Calendar_Model_Attender extends Tinebase_Record_Abstract
18 {
19     /**
20      * supported user types
21      */
22     const USERTYPE_USER        = 'user';
23     const USERTYPE_GROUP       = 'group';
24     const USERTYPE_GROUPMEMBER = 'groupmember';
25     const USERTYPE_RESOURCE    = 'resource';
26     const USERTYPE_LIST        = 'list';
27     
28     /**
29      * supported roles
30      */
31     const ROLE_REQUIRED        = 'REQ';
32     const ROLE_OPTIONAL        = 'OPT';
33     
34     /**
35      * supported status
36      */
37     const STATUS_NEEDSACTION   = 'NEEDS-ACTION';
38     const STATUS_ACCEPTED      = 'ACCEPTED';
39     const STATUS_DECLINED      = 'DECLINED';
40     const STATUS_TENTATIVE     = 'TENTATIVE';
41     
42     /**
43      * cache for already resolved attendee
44      * 
45      * @var array type => array of id => object
46      */
47     protected static $_resovedAttendeeCache = array();
48     
49     /**
50      * key in $_validators/$_properties array for the filed which 
51      * represents the identifier
52      * 
53      * @var string
54      */
55     protected $_identifier = 'id';
56     
57     /**
58      * application the record belongs to
59      *
60      * @var string
61      */
62     protected $_application = 'Calendar';
63     
64     /**
65      * validators
66      *
67      * @var array
68      */
69     protected $_validators = array(
70         // tine record fields
71         'id'                   => array('allowEmpty' => true,  'Alnum'),
72         'created_by'           => array('allowEmpty' => true          ),
73         'creation_time'        => array('allowEmpty' => true          ),
74         'last_modified_by'     => array('allowEmpty' => true          ),
75         'last_modified_time'   => array('allowEmpty' => true          ),
76         'is_deleted'           => array('allowEmpty' => true          ),
77         'deleted_time'         => array('allowEmpty' => true          ),
78         'deleted_by'           => array('allowEmpty' => true          ),
79         'seq'                  => array('allowEmpty' => true,  'Int'  ),
80         
81         'cal_event_id'         => array('allowEmpty' => true/*,  'Alnum'*/),
82         'user_id'              => array('allowEmpty' => false,        ),
83         'user_type'            => array(
84             'allowEmpty' => true,
85             array('InArray', array(self::USERTYPE_USER, self::USERTYPE_GROUP, self::USERTYPE_GROUPMEMBER, self::USERTYPE_RESOURCE))
86         ),
87         'role'                 => array('allowEmpty' => true          ),
88         'quantity'             => array('allowEmpty' => true, 'Int'   ),
89         'status'               => array('allowEmpty' => true          ),
90         'status_authkey'       => array('allowEmpty' => true, 'Alnum' ),
91         'displaycontainer_id'  => array('allowEmpty' => true, 'Int'   ),
92         'transp'               => array(
93             'allowEmpty' => true,
94             array('InArray', array(Calendar_Model_Event::TRANSP_TRANSP, Calendar_Model_Event::TRANSP_OPAQUE))
95         ),
96     );
97     
98     /**
99      * returns accountId of this attender if present
100      * 
101      * @return string
102      */
103     public function getUserAccountId()
104     {
105         if (! in_array($this->user_type, array(self::USERTYPE_USER, self::USERTYPE_GROUPMEMBER))) {
106             return NULL;
107         }
108         
109         try {
110             $contact = Addressbook_Controller_Contact::getInstance()->get($this->user_id, null, false);
111             return $contact->account_id ? $contact->account_id : NULL;
112         } catch (Exception $e) {
113             return NULL;
114         }
115     }
116     
117     /**
118      * get email of attender if exists
119      * 
120      * @return string
121      */
122     public function getEmail()
123     {
124         $resolvedUser = $this->getResolvedUser();
125         if (! $resolvedUser instanceof Tinebase_Record_Abstract) {
126             return '';
127         }
128         
129         switch ($this->user_type) {
130             case self::USERTYPE_USER:
131             case self::USERTYPE_GROUPMEMBER:
132                 return $resolvedUser->getPreferedEmailAddress();
133                 break;
134             case self::USERTYPE_GROUP:
135                 return $resolvedUser->getId();
136                 break;
137             case self::USERTYPE_RESOURCE:
138                 return $resolvedUser->email;
139                 break;
140             default:
141                 throw new Exception("type $type not yet supported");
142                 break;
143         }
144     }
145     
146     /**
147      * get name of attender
148      * 
149      * @return string
150      */
151     public function getName()
152     {
153         $resolvedUser = $this->getResolvedUser();
154         if (! $resolvedUser instanceof Tinebase_Record_Abstract) {
155             Tinebase_Translation::getTranslation('Calendar');
156             return Tinebase_Translation::getTranslation('Calendar')->_('unknown');
157         }
158         
159         switch ($this->user_type) {
160             case self::USERTYPE_USER:
161             case self::USERTYPE_GROUPMEMBER:
162                 return $resolvedUser->n_fileas;
163                 break;
164             case self::USERTYPE_GROUP:
165             case self::USERTYPE_RESOURCE:
166                 return $resolvedUser->name;
167                 break;
168             default:
169                 throw new Exception("type $type not yet supported");
170                 break;
171         }
172     }
173     
174     /**
175      * returns the resolved user_id
176      * 
177      * @return Tinebase_Record_Abstract
178      */
179     public function getResolvedUser()
180     {
181         $clone = clone $this;
182         $resolvable = new Tinebase_Record_RecordSet('Calendar_Model_Attender', array($clone));
183         self::resolveAttendee($resolvable);
184         
185         if ($this->user_type === self::USERTYPE_RESOURCE) {
186             $resource = $clone->user_id;
187             // return pseudo contact with resource data
188             $result = new Addressbook_Model_Contact(array(
189                 'n_family'  => $resource->name,
190                 'email'     => $resource->email,
191                 'id'        => $resource->getId(),
192             ));
193         } else {
194             $result = $clone->user_id;
195         }
196         
197         return $result;
198     }
199     
200     public function getStatusString()
201     {
202         $statusConfig = Calendar_Config::getInstance()->attendeeStatus;
203         $statusRecord = $statusConfig && $statusConfig->records instanceof Tinebase_Record_RecordSet ? $statusConfig->records->getById($this->status) : false;
204         
205         return $statusRecord ? $statusRecord->value : $this->status;
206     }
207     
208     public function getRoleString()
209     {
210         $rolesConfig = Calendar_Config::getInstance()->attendeeRoles;
211         $rolesRecord = $rolesConfig && $rolesConfig->records instanceof Tinebase_Record_RecordSet ? $rolesConfig->records->getById($this->role) : false;
212         
213         return $rolesRecord? $rolesRecord->value : $this->role;
214     }
215     
216     /**
217      * sets the record related properties from user generated input.
218      * 
219      * Input-filtering and validation by Zend_Filter_Input can enabled and disabled
220      *
221      * @param array $_data            the new data to set
222      * @throws Tinebase_Exception_Record_Validation when content contains invalid or missing data
223      */
224     public function setFromArray(array $_data)
225     {
226         if (isset($_data['displaycontainer_id']) && is_array($_data['displaycontainer_id'])) {
227             $_data['displaycontainer_id'] = $_data['displaycontainer_id']['id'];
228         }
229         
230         if (isset($_data['user_id']) && is_array($_data['user_id'])) {
231             if ((isset($_data['user_id']['accountId']) || array_key_exists('accountId', $_data['user_id']))) {
232                 // NOTE: we need to support accounts, cause the client might not have the contact, e.g. when the attender is generated from a container owner
233                 $_data['user_id'] = Addressbook_Controller_Contact::getInstance()->getContactByUserId($_data['user_id']['accountId'], TRUE)->getId();
234             } elseif ((isset($_data['user_id']['group_id']) || array_key_exists('group_id', $_data['user_id']))) {
235                 $_data['user_id'] = is_array($_data['user_id']['group_id']) ? $_data['user_id']['group_id'][0] : $_data['user_id']['group_id'];
236             } else if ((isset($_data['user_id']['id']) || array_key_exists('id', $_data['user_id']))) {
237                 $_data['user_id'] = $_data['user_id']['id'];
238             }
239         }
240         
241         if (empty($_data['quantity'])) {
242             $_data['quantity'] = 1;
243         }
244         
245         parent::setFromArray($_data);
246     }
247     
248     /**
249      * converts an array of emails to a recordSet of attendee for given record
250      * 
251      * @param  Calendar_Model_Event $_event
252      * @param  iteratable           $_emails
253      * @param  bool                 $_implicitAddMissingContacts
254      */
255     public static function emailsToAttendee(Calendar_Model_Event $_event, $_emails, $_implicitAddMissingContacts = TRUE)
256     {
257         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
258             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " list of new attendees " . print_r($_emails, true));
259         
260         if (! $_event->attendee instanceof Tinebase_Record_RecordSet) {
261             $_event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
262         }
263                 
264         // resolve current attendee
265         self::resolveAttendee($_event->attendee);
266         
267         // build currentMailMap
268         // NOTE: non resolvable attendee will be discarded in the map
269         //       this is _important_ for the calculation of migration as it
270         //       saves us from deleting attendee out of current users scope
271         $emailsOfCurrentAttendees = array();
272         foreach ($_event->attendee as $currentAttendee) {
273             if ($currentAttendeeEmailAddress = $currentAttendee->getEmail()) {
274                 $emailsOfCurrentAttendees[$currentAttendeeEmailAddress] = $currentAttendee;
275             }
276         }
277         
278         // collect emails of new attendees (skipping if no email present)
279         $emailsOfNewAttendees = array();
280         foreach ($_emails as $newAttendee) {
281             if ($newAttendee['email']) {
282                 $emailsOfNewAttendees[$newAttendee['email']] = $newAttendee;
283             }
284         }
285         
286         // attendees to remove
287         $attendeesToDelete = array_diff_key($emailsOfCurrentAttendees, $emailsOfNewAttendees);
288         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " attendees to delete " . print_r(array_keys($attendeesToDelete), true));
289         
290         // delete attendees no longer attending from recordset
291         foreach ($attendeesToDelete as $attendeeToDelete) {
292             $_event->attendee->removeRecord($attendeeToDelete);
293         }
294         
295         // attendees to keep and update
296         $attendeesToKeep   = array_diff_key($emailsOfCurrentAttendees, $attendeesToDelete);
297         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " attendees to keep " . print_r(array_keys($attendeesToKeep), true));
298         //var_dump($attendeesToKeep);
299         foreach($attendeesToKeep as $emailAddress => $attendeeToKeep) {
300             $newSettings = $emailsOfNewAttendees[$emailAddress];
301
302             // update object by reference
303             $attendeeToKeep->status = isset($newSettings['partStat']) ? $newSettings['partStat'] : $attendeeToKeep->status;
304             $attendeeToKeep->role   = $newSettings['role'];
305         }
306
307         // new attendess to add to event
308         $attendeesToAdd    = array_diff_key($emailsOfNewAttendees,     $emailsOfCurrentAttendees);
309         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " attendees to add " . print_r(array_keys($attendeesToAdd), true));
310         
311         $smtpConfig = Tinebase_Config::getInstance()->get(Tinebase_Model_Config::SMTP, new Tinebase_Config_Struct())->toArray();
312         
313         // add attendee identified by their emailAdress
314         foreach ($attendeesToAdd as $newAttendee) {
315             $attendeeId = NULL;
316             
317             if ($newAttendee['userType'] == Calendar_Model_Attender::USERTYPE_USER) {
318                 // does a contact with this email address exist?
319                 if ($contact = self::resolveEmailToContact($newAttendee, false)) {
320                     $attendeeId = $contact->getId();
321                     
322                 }
323                 
324                 // does a resouce with this email address exist?
325                 if ( ! $attendeeId) {
326                     $resources = Calendar_Controller_Resource::getInstance()->search(new Calendar_Model_ResourceFilter(array(
327                         array('field' => 'email', 'operator' => 'equals', 'value' => $newAttendee['email']),
328                     )));
329                     
330                     if(count($resources) > 0) {
331                         $newAttendee['userType'] = Calendar_Model_Attender::USERTYPE_RESOURCE;
332                         $attendeeId = $resources->getFirstRecord()->getId();
333                     }
334                 }
335                 // does a list with this name exist?
336                 if ( ! $attendeeId &&
337                     isset($smtpConfig['primarydomain']) && 
338                     preg_match('/(?P<localName>.*)@' . preg_quote($smtpConfig['primarydomain']) . '$/', $newAttendee['email'], $matches)
339                 ) {
340                     $lists = Addressbook_Controller_List::getInstance()->search(new Addressbook_Model_ListFilter(array(
341                         array('field' => 'name',       'operator' => 'equals', 'value' => $matches['localName']),
342                         array('field' => 'type',       'operator' => 'equals', 'value' => Addressbook_Model_List::LISTTYPE_GROUP),
343                         array('field' => 'showHidden', 'operator' => 'equals', 'value' => TRUE),
344                     )));
345                     
346                     if(count($lists) > 0) {
347                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
348                             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " found # of lists " . count($lists));
349                     
350                         $newAttendee['userType'] = Calendar_Model_Attender::USERTYPE_GROUP;
351                         $attendeeId = $lists->getFirstRecord()->group_id;
352                     }
353                 } 
354                 
355                 if (! $attendeeId) {
356                     // autocreate a contact if allowed
357                     $contact = self::resolveEmailToContact($newAttendee, $_implicitAddMissingContacts);
358                     if ($contact) {
359                         $attendeeId = $contact->getId();
360                     }
361                 }
362             } else if($newAttendee['userType'] == Calendar_Model_Attender::USERTYPE_GROUP) {
363                 $lists = Addressbook_Controller_List::getInstance()->search(new Addressbook_Model_ListFilter(array(
364                     array('field' => 'name',       'operator' => 'equals', 'value' => $newAttendee['displayName']),
365                     array('field' => 'type',       'operator' => 'equals', 'value' => Addressbook_Model_List::LISTTYPE_GROUP),
366                     array('field' => 'showHidden', 'operator' => 'equals', 'value' => TRUE),
367                 )));
368                 
369                 if(count($lists) > 0) {
370                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " found # of lists " . count($lists));
371                 
372                     $attendeeId = $lists->getFirstRecord()->group_id;
373                 }
374             }
375             
376             if ($attendeeId !== NULL) {
377                 // finally add to attendee
378                 $_event->attendee->addRecord(new Calendar_Model_Attender(array(
379                     'user_id'   => $attendeeId,
380                     'user_type' => $newAttendee['userType'],
381                     'status'    => isset($newAttendee['partStat']) ? $newAttendee['partStat'] : self::STATUS_NEEDSACTION,
382                     'role'      => $newAttendee['role']
383                 )));
384             }
385         }
386         
387         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
388             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " updated attendees list " . print_r($_event->attendee->toArray(), true));
389     }
390     
391     /**
392      * get attendee with user_id = email address and create contacts for them on the fly if they do not exist
393      * 
394      * @param Calendar_Model_Event $_event
395      * @throws Tinebase_Exception_InvalidArgument
396      */
397     public static function resolveEmailOnlyAttendee(Calendar_Model_Event $_event)
398     {
399         if (! $_event->attendee instanceof Tinebase_Record_RecordSet) {
400             $_event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
401         }
402         
403         foreach ($_event->attendee as $currentAttendee) {
404             if (is_string($currentAttendee->user_id) && preg_match(Tinebase_Mail::EMAIL_ADDRESS_REGEXP, $currentAttendee->user_id)) {
405                 if ($currentAttendee->user_type !== Calendar_Model_Attender::USERTYPE_USER) {
406                     throw new Tinebase_Exception_InvalidArgument('it is only allowed to set contacts as email only attender');
407                 }
408                 $contact = self::resolveEmailToContact(array(
409                     'email'     => $currentAttendee->user_id,
410                 ));
411                 $currentAttendee->user_id = $contact->getId();
412             }
413         }
414     }
415     
416    /**
417     * check if contact with given email exists in addressbook and creates it if not
418     *
419     * @param  array $_attenderData array with email, firstname and lastname (if available)
420     * @param  boolean $_implicitAddMissingContacts
421     * @return Addressbook_Model_Contact
422     * 
423     * @todo filter by fn if multiple matches
424     */
425     public static function resolveEmailToContact($_attenderData, $_implicitAddMissingContacts = TRUE)
426     {
427         if (! isset($_attenderData['email']) || empty($_attenderData['email'])) {
428             throw new Tinebase_Exception_InvalidArgument('email address is needed to resolve contact');
429         }
430         
431         $email = self::_sanitizeEmail($_attenderData['email']);
432         
433         $contacts = Addressbook_Controller_Contact::getInstance()->search(new Addressbook_Model_ContactFilter(array(
434             array('condition' => 'OR', 'filters' => array(
435                 array('field' => 'email',      'operator'  => 'equals', 'value' => $email),
436                 array('field' => 'email_home', 'operator'  => 'equals', 'value' => $email)
437             )),
438         )), new Tinebase_Model_Pagination(array(
439             'sort'    => 'type', // prefer user over contact
440             'dir'     => 'DESC',
441             'limit'   => 1
442         )));
443         
444         if (count($contacts) > 0) {
445             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
446                     . " Found # of contacts " . count($contacts));
447             $result = $contacts->getFirstRecord();
448         
449         } else if ($_implicitAddMissingContacts === TRUE) {
450             $translation = Tinebase_Translation::getTranslation('Calendar');
451             $i18nNote = $translation->_('This contact has been automatically added by the system as an event attender');
452             if ($email !== $_attenderData['email']) {
453                 $i18nNote .= "\n";
454                 $i18nNote .= $translation->_('The email address has been shortened:') . ' ' . $_attenderData['email'] . ' -> ' . $email;
455             }
456             $contactData = array(
457                 'note'        => $i18nNote,
458                 'email'       => $email,
459                 'n_family'    => (isset($_attenderData['lastName']) && ! empty($_attenderData['lastName'])) ? $_attenderData['lastName'] : $email,
460                 'n_given'     => (isset($_attenderData['firstName'])) ? $_attenderData['firstName'] : '',
461             );
462             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
463                     . " Ádd new contact " . print_r($contactData, true));
464             $contact = new Addressbook_Model_Contact($contactData);
465             $result = Addressbook_Controller_Contact::getInstance()->create($contact, FALSE);
466         } else {
467             $result = NULL;
468         }
469         
470         return $result;
471     }
472     
473     /**
474      * sanitize email address
475      * 
476      * @param string $email
477      * @return string
478      * @throws Tinebase_Exception_Record_Validation
479      */
480     protected static function _sanitizeEmail($email)
481     {
482         // TODO should be generalized OR increase size of email field(s)
483         $result = $email;
484         if (strlen($email) > 64) {
485             // try to find '/' for splitting
486             $lastSlash = strrpos($email, '/');
487             if ($lastSlash !== false) {
488                 $result = substr($email, $lastSlash + 1);
489             }
490             
491             if (strlen($result) > 64) {
492                 // try to find first valid email
493                 if (preg_match(Tinebase_Mail::EMAIL_ADDRESS_REGEXP, $result, $matches)) {
494                     $result = $matches[0];
495                 }
496                 
497                 if (strlen($result) > 64) {
498                     if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
499                         . ' Email address could not be sanitized: ' . $email . '(length: ' . strlen($email) . ')');
500                     throw new Tinebase_Exception_Record_Validation('email string too long');
501                 }
502             } else {
503                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
504                     . ' Email address has been sanitized: ' . $email . ' -> ' . $result);
505             }
506         }
507         
508         return $result;
509     }
510     
511     /**
512      * resolves group members and adds/removes them if nesesary
513      * 
514      * NOTE: If a user is listed as user and as groupmember, we supress the groupmember
515      * 
516      * NOTE: The role to assign to a new group member is not always clear, as multiple groups
517      *       might be the 'source' of the group member. To deal with this, we take the role of
518      *       the first group when we add new group members
519      *       
520      * @param Tinebase_Record_RecordSet $_attendee
521      * @return void
522      */
523     public static function resolveGroupMembers($_attendee)
524     {
525         if (! $_attendee instanceof Tinebase_Record_RecordSet) {
526             return;
527         }
528         $_attendee->addIndices(array('user_type'));
529         
530         // flatten user_ids (not groups for group/list handling bellow)
531         foreach($_attendee as $attendee) {
532             if ($attendee->user_type != Calendar_Model_Attender::USERTYPE_GROUP && $attendee->user_id instanceof Tinebase_Record_Abstract) {
533                 $attendee->user_id = $attendee->user_id->getId();
534             }
535         }
536         
537         $groupAttendee = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUP);
538         
539         $allCurrGroupMembers = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
540         $allCurrGroupMembersContactIds = $allCurrGroupMembers->user_id;
541         
542         $allGroupMembersContactIds = array();
543         foreach ($groupAttendee as $groupAttender) {
544             #$groupAttenderMemberIds = Tinebase_Group::getInstance()->getGroupMembers($groupAttender->user_id);
545             #$groupAttenderContactIds = Tinebase_User::getInstance()->getMultiple($groupAttenderMemberIds)->contact_id;
546             #$allGroupMembersContactIds = array_merge($allGroupMembersContactIds, $groupAttenderContactIds);
547             
548             $listId = null;
549         
550             if ($groupAttender->user_id instanceof Addressbook_Model_List) {
551                 $listId = $groupAttender->user_id->getId();
552             } else if ($groupAttender->user_id !== NULL) {
553                 $group = Tinebase_Group::getInstance()->getGroupById($groupAttender->user_id);
554                 if (!empty($group->list_id)) {
555                     $listId = $group->list_id;
556                 }
557             } else {
558                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
559                     . ' Group attender ID missing');
560                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
561                     . ' ' . print_r($groupAttender->toArray(), TRUE));
562             }
563             
564             if ($listId !== null) {
565                 $groupAttenderContactIds = Addressbook_Controller_List::getInstance()->get($listId)->members;
566                 $allGroupMembersContactIds = array_merge($allGroupMembersContactIds, $groupAttenderContactIds);
567                 
568                 $toAdd = array_diff($groupAttenderContactIds, $allCurrGroupMembersContactIds);
569                 
570                 foreach($toAdd as $userId) {
571                     $_attendee->addRecord(new Calendar_Model_Attender(array(
572                         'user_type' => Calendar_Model_Attender::USERTYPE_GROUPMEMBER,
573                         'user_id'   => $userId,
574                         'role'      => $groupAttender->role
575                     )));
576                 }
577             }
578         }
579         
580         $toDel = array_diff($allCurrGroupMembersContactIds, $allGroupMembersContactIds);
581         foreach ($toDel as $idx => $contactId) {
582             $attender = $allCurrGroupMembers->find('user_id', $contactId);
583             $_attendee->removeRecord($attender);
584         }
585         
586         // calculate double members (groupmember + user)
587         $groupmembers = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
588         $users        = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_USER);
589         $doublicates = array_intersect($users->user_id, $groupmembers->user_id);
590         foreach ($doublicates as $user_id) {
591             $attender = $groupmembers->find('user_id', $user_id);
592             $_attendee->removeRecord($attender);
593         }
594     }
595     
596     /**
597      * get own attender
598      * 
599      * @param Tinebase_Record_RecordSet $_attendee
600      * @return Calendar_Model_Attender|NULL
601      */
602     public static function getOwnAttender($_attendee)
603     {
604         return self::getAttendee($_attendee, new Calendar_Model_Attender(array(
605             'user_id'   => Tinebase_Core::getUser()->contact_id,
606             'user_type' => Calendar_Model_Attender::USERTYPE_USER
607         )));
608     }
609     
610     /**
611      * get a single attendee from set of attendee
612      * 
613      * @param Tinebase_Record_RecordSet $_attendeeSet
614      * @param Calendar_Model_Attender $_attendee
615      * @return Calendar_Model_Attender|NULL
616      */
617     public static function getAttendee($_attendeeSet, $_attendee)
618     {
619         $attendeeSet  = $_attendeeSet instanceof Tinebase_Record_RecordSet ? clone $_attendeeSet : new Tinebase_Record_RecordSet('Calendar_Model_Attender');
620         $attendeeSet->addIndices(array('user_type', 'user_id'));
621         
622         // transform id to string
623         foreach ($attendeeSet as $attendee) {
624             $attendee->user_id  = $attendee->user_id instanceof Tinebase_Record_Abstract ? $attendee->user_id->getId() : $attendee->user_id;
625         }
626         
627         $attendeeUserId = $_attendee->user_id instanceof Tinebase_Record_Abstract ? $_attendee->user_id->getId() : $_attendee->user_id;
628         
629         $foundAttendee = $attendeeSet
630             ->filter('user_type', $_attendee->user_type)
631             ->filter('user_id', $attendeeUserId)
632             ->getFirstRecord();
633         
634         // search for groupmember if no user got found
635         if ($foundAttendee === null && $_attendee->user_type == Calendar_Model_Attender::USERTYPE_USER) {
636             $foundAttendee = $attendeeSet
637                 ->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER)
638                 ->filter('user_id', $attendeeUserId)
639                 ->getFirstRecord();
640         }
641             
642         return $foundAttendee ? $_attendeeSet[$attendeeSet->indexOf($foundAttendee)] : NULL;
643         
644     }
645     
646     /**
647      * returns migration of two attendee sets
648      * 
649      * @param  Tinebase_Record_RecordSet $_current
650      * @param  Tinebase_Record_RecordSet $_update
651      * @return array migrationKey => Tinebase_Record_RecordSet
652      */
653     public static function getMigration($_current, $_update)
654     {
655         $result = array(
656             'toDelete' => new Tinebase_Record_RecordSet('Calendar_Model_Attender'),
657             'toCreate' => clone $_update,
658             'toUpdate' => new Tinebase_Record_RecordSet('Calendar_Model_Attender'),
659         );
660         
661         foreach($_current as $currAttendee) {
662             $updateAttendee = self::getAttendee($result['toCreate'], $currAttendee);
663             if ($updateAttendee) {
664                 $result['toUpdate']->addRecord($updateAttendee);
665                 $result['toCreate']->removeRecord($updateAttendee);
666             } else {
667                 $result['toDelete']->addRecord($currAttendee);
668             }
669         }
670         
671         return $result;
672     }
673     
674     /**
675      * resolves given attendee for json representation
676      * 
677      * @TODO move status_authkey cleanup elsewhere
678      * 
679      * @param Tinebase_Record_RecordSet|array   $_eventAttendee 
680      * @param bool                              $_resolveDisplayContainers
681      * @param Calendar_Model_Event|array        $_events
682      */
683     public static function resolveAttendee($_eventAttendee, $_resolveDisplayContainers = TRUE, $_events = NULL)
684     {
685         if (empty($_eventAttendee)) {
686             return;
687         }
688         
689         $eventAttendee = $_eventAttendee instanceof Tinebase_Record_RecordSet ? array($_eventAttendee) : $_eventAttendee;
690         $events = $_events instanceof Tinebase_Record_Abstract ? array($_events) : $_events;
691         
692         // set containing all attendee
693         $allAttendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
694         $typeMap = array();
695         
696         // build type map 
697         foreach ($eventAttendee as $attendee) {
698             foreach ($attendee as $attender) {
699                 $allAttendee->addRecord($attender);
700             
701                 if ($attender->user_id instanceof Tinebase_Record_Abstract) {
702                     // already resolved
703                     continue;
704                 } elseif ((isset(self::$_resovedAttendeeCache[$attender->user_type]) || array_key_exists($attender->user_type, self::$_resovedAttendeeCache)) && (isset(self::$_resovedAttendeeCache[$attender->user_type][$attender->user_id]) || array_key_exists($attender->user_id, self::$_resovedAttendeeCache[$attender->user_type]))){
705                     // already in cache
706                     $attender->user_id = self::$_resovedAttendeeCache[$attender->user_type][$attender->user_id];
707                 } else {
708                     if (! (isset($typeMap[$attender->user_type]) || array_key_exists($attender->user_type, $typeMap))) {
709                         $typeMap[$attender->user_type] = array();
710                     }
711                     $typeMap[$attender->user_type][] = $attender->user_id;
712                 }
713             }
714         }
715         
716         // resolve display containers
717         if ($_resolveDisplayContainers) {
718             $displaycontainerIds = array_diff($allAttendee->displaycontainer_id, array(''));
719             if (! empty($displaycontainerIds)) {
720                 Tinebase_Container::getInstance()->getGrantsOfRecords($allAttendee, Tinebase_Core::getUser(), 'displaycontainer_id');
721             }
722         }
723         
724         // get all user_id entries
725         foreach ($typeMap as $type => $ids) {
726             switch ($type) {
727                 case self::USERTYPE_USER:
728                 case self::USERTYPE_GROUPMEMBER:
729                     $resolveCf = Addressbook_Controller_Contact::getInstance()->resolveCustomfields(FALSE);
730                     $typeMap[$type] = Addressbook_Controller_Contact::getInstance()->getMultiple(array_unique($ids), TRUE);
731                     Addressbook_Controller_Contact::getInstance()->resolveCustomfields($resolveCf);
732                     break;
733                 case self::USERTYPE_GROUP:
734                 case Calendar_Model_AttenderFilter::USERTYPE_MEMBEROF:
735                     // first fetch the groups, then the lists identified by list_id
736                     $typeMap[$type] = Tinebase_Group::getInstance()->getMultiple(array_unique($ids));
737                     $typeMap[self::USERTYPE_LIST] = Addressbook_Controller_List::getInstance()->getMultiple($typeMap[$type]->list_id, true);
738                     break;
739                 case self::USERTYPE_RESOURCE:
740                     $typeMap[$type] = Calendar_Controller_Resource::getInstance()->getMultiple(array_unique($ids), true);
741                     break;
742                 default:
743                     throw new Exception("type $type not supported");
744                     break;
745             }
746         }
747         
748         // sort entries in
749         foreach ($eventAttendee as $attendee) {
750             foreach ($attendee as $attender) {
751                 if ($attender->user_id instanceof Tinebase_Record_Abstract) {
752                     // allready resolved from cache
753                     continue;
754                 }
755
756                 $idx = false;
757                 
758                 if ($attender->user_type == self::USERTYPE_GROUP) {
759                     $attendeeTypeSet = $typeMap[$attender->user_type];
760                     $idx = $attendeeTypeSet->getIndexById($attender->user_id);
761                     if ($idx !== false) {
762                         $group = $attendeeTypeSet[$idx];
763                         
764                         $idx = false;
765                         
766                         $attendeeTypeSet = $typeMap[self::USERTYPE_LIST];
767                         $idx = $attendeeTypeSet->getIndexById($group->list_id);
768                     } 
769                 } else {
770                     $attendeeTypeSet = $typeMap[$attender->user_type];
771                     $idx = $attendeeTypeSet->getIndexById($attender->user_id);
772                 }
773                 if ($idx !== false) {
774                     // copy to cache
775                     if (! (isset(self::$_resovedAttendeeCache[$attender->user_type]) || array_key_exists($attender->user_type, self::$_resovedAttendeeCache))) {
776                         self::$_resovedAttendeeCache[$attender->user_type] = array();
777                     }
778                     self::$_resovedAttendeeCache[$attender->user_type][$attender->user_id] = $attendeeTypeSet[$idx];
779                     
780                     $attender->user_id = $attendeeTypeSet[$idx];
781                 }
782             }
783         }
784         
785         
786         foreach ($eventAttendee as $idx => $attendee) {
787             $event = is_array($events) && (isset($events[$idx]) || array_key_exists($idx, $events)) ? $events[$idx] : NULL;
788             
789             foreach ($attendee as $attender) {
790                 // keep authkey if user has editGrant to displaycontainer
791                 if (isset($attender['displaycontainer_id']) && !is_scalar($attender['displaycontainer_id']) && (isset($attender['displaycontainer_id']['account_grants'][Tinebase_Model_Grants::GRANT_EDIT]) || array_key_exists(Tinebase_Model_Grants::GRANT_EDIT, $attender['displaycontainer_id']['account_grants'])) &&  $attender['displaycontainer_id']['account_grants'][Tinebase_Model_Grants::GRANT_EDIT]) {
792                     continue;
793                 }
794                 
795                 // keep authkey if attender is a contact (no account) and user has editGrant for event
796                 if ($attender->user_type == self::USERTYPE_USER
797                     && $attender->user_id instanceof Tinebase_Record_Abstract
798                     && (!$attender->user_id->has('account_id') || !$attender->user_id->account_id)
799                     && (!$event || $event->{Tinebase_Model_Grants::GRANT_EDIT})
800                 ) {
801                     continue;
802                 }
803                 
804                 $attender->status_authkey = NULL;
805             }
806         }
807     }
808     
809     /**
810      * checks if given alarm should be send to given attendee
811      * 
812      * @param  Calendar_Model_Attender $_attendee
813      * @param  Tinebase_Model_Alarm    $_alarm
814      * @return bool
815      */
816     public static function isAlarmForAttendee($_attendee, $_alarm, $_event=NULL)
817     {
818         // attendee: array with one user_type/id if alarm is for one attendee only
819         $attendeeOption = $_alarm->getOption('attendee');
820         
821         // skip: array of array of user_type/id with attendees this alarm is to skip for
822         $skipOption = $_alarm->getOption('skip');
823         
824         if ($attendeeOption) {
825             return (bool) self::getAttendee(new Tinebase_Record_RecordSet('Calendar_Model_Attender', array($_attendee)), new Calendar_Model_Attender($attendeeOption));
826         }
827         
828         if (is_array($skipOption)) {
829             $skipAttendees = new Tinebase_Record_RecordSet('Calendar_Model_Attender', $skipOption);
830             if(self::getAttendee($skipAttendees, $_attendee)) {
831                 return false;
832             }
833         }
834         
835         $isOrganizerCondition = $_event ? $_event->isOrganizer($_attendee) : TRUE;
836         $isAttendeeCondition = $_event && $_event->attendee instanceof Tinebase_Record_RecordSet ? self::getAttendee($_event->attendee, $_attendee) : TRUE;
837         return ($isAttendeeCondition || $isOrganizerCondition)&& $_attendee->status != Calendar_Model_Attender::STATUS_DECLINED;
838     }
839 }