implicit resource read write as calendar attendee
[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'])) {
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__ . " found # of contacts " . count($contacts));
446             $result = $contacts->getFirstRecord();
447         
448         } else if ($_implicitAddMissingContacts === TRUE) {
449             $translation = Tinebase_Translation::getTranslation('Calendar');
450             $i18nNote = $translation->_('This contact has been automatically added by the system as an event attender');
451             if ($email !== $_attenderData['email']) {
452                 $i18nNote .= "\n";
453                 $i18nNote .= $translation->_('The email address has been shortened: ') . $_attenderData['email'] . ' -> ' . $email;
454             }
455             $contactData = array(
456                 'note'        => $i18nNote,
457                 'email'       => $email,
458                 'n_family'    => (isset($_attenderData['lastName'])) ? $_attenderData['lastName'] : $email,
459                 'n_given'     => (isset($_attenderData['firstName'])) ? $_attenderData['firstName'] : '',
460             );
461             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " add new contact " . print_r($contactData, true));
462             $contact = new Addressbook_Model_Contact($contactData);
463             $result = Addressbook_Controller_Contact::getInstance()->create($contact, FALSE);
464         } else {
465             $result = NULL;
466         }
467         
468         return $result;
469     }
470     
471     /**
472      * sanitize email address
473      * 
474      * @param string $email
475      * @return string
476      * @throws Tinebase_Exception_Record_Validation
477      */
478     protected static function _sanitizeEmail($email)
479     {
480         // TODO should be generalized OR increase size of email field(s)
481         $result = $email;
482         if (strlen($email) > 64) {
483             // try to find '/' for splitting
484             $lastSlash = strrpos($email, '/');
485             if ($lastSlash !== false) {
486                 $result = substr($email, $lastSlash + 1);
487             }
488             
489             if (strlen($result) > 64) {
490                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
491                     . ' Email address could not be sanitized: ' . $email . '(length: ' . strlen($email) . ')');
492                 throw new Tinebase_Exception_Record_Validation('email string too long');
493             } else {
494                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
495                     . ' Email address has been sanitized: ' . $email . ' -> ' . $result);
496             }
497         }
498         
499         return $result;
500     }
501     
502     /**
503      * resolves group members and adds/removes them if nesesary
504      * 
505      * NOTE: If a user is listed as user and as groupmember, we supress the groupmember
506      * 
507      * NOTE: The role to assign to a new group member is not always clear, as multiple groups
508      *       might be the 'source' of the group member. To deal with this, we take the role of
509      *       the first group when we add new group members
510      *       
511      * @param Tinebase_Record_RecordSet $_attendee
512      * @return void
513      */
514     public static function resolveGroupMembers($_attendee)
515     {
516         if (! $_attendee instanceof Tinebase_Record_RecordSet) {
517             return;
518         }
519         $_attendee->addIndices(array('user_type'));
520         
521         // flatten user_ids (not groups for group/list handling bellow)
522         foreach($_attendee as $attendee) {
523             if ($attendee->user_type != Calendar_Model_Attender::USERTYPE_GROUP && $attendee->user_id instanceof Tinebase_Record_Abstract) {
524                 $attendee->user_id = $attendee->user_id->getId();
525             }
526         }
527         
528         $groupAttendee = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUP);
529         
530         $allCurrGroupMembers = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
531         $allCurrGroupMembersContactIds = $allCurrGroupMembers->user_id;
532         
533         $allGroupMembersContactIds = array();
534         foreach ($groupAttendee as $groupAttender) {
535             #$groupAttenderMemberIds = Tinebase_Group::getInstance()->getGroupMembers($groupAttender->user_id);
536             #$groupAttenderContactIds = Tinebase_User::getInstance()->getMultiple($groupAttenderMemberIds)->contact_id;
537             #$allGroupMembersContactIds = array_merge($allGroupMembersContactIds, $groupAttenderContactIds);
538             
539             $listId = null;
540         
541             if ($groupAttender->user_id instanceof Addressbook_Model_List) {
542                 $listId = $groupAttender->user_id->getId();
543             } else if ($groupAttender->user_id !== NULL) {
544                 $group = Tinebase_Group::getInstance()->getGroupById($groupAttender->user_id);
545                 if (!empty($group->list_id)) {
546                     $listId = $group->list_id;
547                 }
548             } else {
549                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
550                     . ' Group attender ID missing');
551                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
552                     . ' ' . print_r($groupAttender->toArray(), TRUE));
553             }
554             
555             if ($listId !== null) {
556                 $groupAttenderContactIds = Addressbook_Controller_List::getInstance()->get($listId)->members;
557                 $allGroupMembersContactIds = array_merge($allGroupMembersContactIds, $groupAttenderContactIds);
558                 
559                 $toAdd = array_diff($groupAttenderContactIds, $allCurrGroupMembersContactIds);
560                 
561                 foreach($toAdd as $userId) {
562                     $_attendee->addRecord(new Calendar_Model_Attender(array(
563                         'user_type' => Calendar_Model_Attender::USERTYPE_GROUPMEMBER,
564                         'user_id'   => $userId,
565                         'role'      => $groupAttender->role
566                     )));
567                 }
568             }
569         }
570         
571         $toDel = array_diff($allCurrGroupMembersContactIds, $allGroupMembersContactIds);
572         foreach ($toDel as $idx => $contactId) {
573             $attender = $allCurrGroupMembers->find('user_id', $contactId);
574             $_attendee->removeRecord($attender);
575         }
576         
577         // calculate double members (groupmember + user)
578         $groupmembers = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
579         $users        = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_USER);
580         $doublicates = array_intersect($users->user_id, $groupmembers->user_id);
581         foreach ($doublicates as $user_id) {
582             $attender = $groupmembers->find('user_id', $user_id);
583             $_attendee->removeRecord($attender);
584         }
585     }
586     
587     /**
588      * get own attender
589      * 
590      * @param Tinebase_Record_RecordSet $_attendee
591      * @return Calendar_Model_Attender|NULL
592      */
593     public static function getOwnAttender($_attendee)
594     {
595         return self::getAttendee($_attendee, new Calendar_Model_Attender(array(
596             'user_id'   => Tinebase_Core::getUser()->contact_id,
597             'user_type' => Calendar_Model_Attender::USERTYPE_USER
598         )));
599     }
600     
601     /**
602      * get a single attendee from set of attendee
603      * 
604      * @param Tinebase_Record_RecordSet $_attendeeSet
605      * @param Calendar_Model_Attender $_attendee
606      * @return Calendar_Model_Attender|NULL
607      */
608     public static function getAttendee($_attendeeSet, $_attendee)
609     {
610         $attendeeSet  = $_attendeeSet instanceof Tinebase_Record_RecordSet ? clone $_attendeeSet : new Tinebase_Record_RecordSet('Calendar_Model_Attender');
611         $attendeeSet->addIndices(array('user_type', 'user_id'));
612         
613         // transform id to string
614         foreach ($attendeeSet as $attendee) {
615             $attendee->user_id  = $attendee->user_id instanceof Tinebase_Record_Abstract ? $attendee->user_id->getId() : $attendee->user_id;
616         }
617         
618         $attendeeUserId = $_attendee->user_id instanceof Tinebase_Record_Abstract ? $_attendee->user_id->getId() : $_attendee->user_id;
619         
620         $foundAttendee = $attendeeSet
621             ->filter('user_type', $_attendee->user_type)
622             ->filter('user_id', $attendeeUserId)
623             ->getFirstRecord();
624         
625         // search for groupmember if no user got found
626         if ($foundAttendee === null && $_attendee->user_type == Calendar_Model_Attender::USERTYPE_USER) {
627             $foundAttendee = $attendeeSet
628                 ->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER)
629                 ->filter('user_id', $attendeeUserId)
630                 ->getFirstRecord();
631         }
632             
633         return $foundAttendee ? $_attendeeSet[$attendeeSet->indexOf($foundAttendee)] : NULL;
634         
635     }
636     
637     /**
638      * returns migration of two attendee sets
639      * 
640      * @param  Tinebase_Record_RecordSet $_current
641      * @param  Tinebase_Record_RecordSet $_update
642      * @return array migrationKey => Tinebase_Record_RecordSet
643      */
644     public static function getMigration($_current, $_update)
645     {
646         $result = array(
647             'toDelete' => new Tinebase_Record_RecordSet('Calendar_Model_Attender'),
648             'toCreate' => clone $_update,
649             'toUpdate' => new Tinebase_Record_RecordSet('Calendar_Model_Attender'),
650         );
651         
652         foreach($_current as $currAttendee) {
653             $updateAttendee = self::getAttendee($result['toCreate'], $currAttendee);
654             if ($updateAttendee) {
655                 $result['toUpdate']->addRecord($updateAttendee);
656                 $result['toCreate']->removeRecord($updateAttendee);
657             } else {
658                 $result['toDelete']->addRecord($currAttendee);
659             }
660         }
661         
662         return $result;
663     }
664     
665     /**
666      * resolves given attendee for json representation
667      * 
668      * @TODO move status_authkey cleanup elsewhere
669      * 
670      * @param Tinebase_Record_RecordSet|array   $_eventAttendee 
671      * @param bool                              $_resolveDisplayContainers
672      * @param Calendar_Model_Event|array        $_events
673      */
674     public static function resolveAttendee($_eventAttendee, $_resolveDisplayContainers = TRUE, $_events = NULL)
675     {
676         if (empty($_eventAttendee)) {
677             return;
678         }
679         
680         $eventAttendee = $_eventAttendee instanceof Tinebase_Record_RecordSet ? array($_eventAttendee) : $_eventAttendee;
681         $events = $_events instanceof Tinebase_Record_Abstract ? array($_events) : $_events;
682         
683         // set containing all attendee
684         $allAttendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
685         $typeMap = array();
686         
687         // build type map 
688         foreach ($eventAttendee as $attendee) {
689             foreach ($attendee as $attender) {
690                 $allAttendee->addRecord($attender);
691             
692                 if ($attender->user_id instanceof Tinebase_Record_Abstract) {
693                     // already resolved
694                     continue;
695                 } 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]))){
696                     // already in cache
697                     $attender->user_id = self::$_resovedAttendeeCache[$attender->user_type][$attender->user_id];
698                 } else {
699                     if (! (isset($typeMap[$attender->user_type]) || array_key_exists($attender->user_type, $typeMap))) {
700                         $typeMap[$attender->user_type] = array();
701                     }
702                     $typeMap[$attender->user_type][] = $attender->user_id;
703                 }
704             }
705         }
706         
707         // resolve display containers
708         if ($_resolveDisplayContainers) {
709             $displaycontainerIds = array_diff($allAttendee->displaycontainer_id, array(''));
710             if (! empty($displaycontainerIds)) {
711                 Tinebase_Container::getInstance()->getGrantsOfRecords($allAttendee, Tinebase_Core::getUser(), 'displaycontainer_id');
712             }
713         }
714         
715         // get all user_id entries
716         foreach ($typeMap as $type => $ids) {
717             switch ($type) {
718                 case self::USERTYPE_USER:
719                 case self::USERTYPE_GROUPMEMBER:
720                     $resolveCf = Addressbook_Controller_Contact::getInstance()->resolveCustomfields(FALSE);
721                     $typeMap[$type] = Addressbook_Controller_Contact::getInstance()->getMultiple(array_unique($ids), TRUE);
722                     Addressbook_Controller_Contact::getInstance()->resolveCustomfields($resolveCf);
723                     break;
724                 case self::USERTYPE_GROUP:
725                 case Calendar_Model_AttenderFilter::USERTYPE_MEMBEROF:
726                     // first fetch the groups, then the lists identified by list_id
727                     $typeMap[$type] = Tinebase_Group::getInstance()->getMultiple(array_unique($ids));
728                     $typeMap[self::USERTYPE_LIST] = Addressbook_Controller_List::getInstance()->getMultiple($typeMap[$type]->list_id, true);
729                     break;
730                 case self::USERTYPE_RESOURCE:
731                     $typeMap[$type] = Calendar_Controller_Resource::getInstance()->getMultiple(array_unique($ids), true);
732                     break;
733                 default:
734                     throw new Exception("type $type not supported");
735                     break;
736             }
737         }
738         
739         // sort entries in
740         foreach ($eventAttendee as $attendee) {
741             foreach ($attendee as $attender) {
742                 if ($attender->user_id instanceof Tinebase_Record_Abstract) {
743                     // allready resolved from cache
744                     continue;
745                 }
746
747                 $idx = false;
748                 
749                 if ($attender->user_type == self::USERTYPE_GROUP) {
750                     $attendeeTypeSet = $typeMap[$attender->user_type];
751                     $idx = $attendeeTypeSet->getIndexById($attender->user_id);
752                     if ($idx !== false) {
753                         $group = $attendeeTypeSet[$idx];
754                         
755                         $idx = false;
756                         
757                         $attendeeTypeSet = $typeMap[self::USERTYPE_LIST];
758                         $idx = $attendeeTypeSet->getIndexById($group->list_id);
759                     } 
760                 } else {
761                     $attendeeTypeSet = $typeMap[$attender->user_type];
762                     $idx = $attendeeTypeSet->getIndexById($attender->user_id);
763                 }
764                 if ($idx !== false) {
765                     // copy to cache
766                     if (! (isset(self::$_resovedAttendeeCache[$attender->user_type]) || array_key_exists($attender->user_type, self::$_resovedAttendeeCache))) {
767                         self::$_resovedAttendeeCache[$attender->user_type] = array();
768                     }
769                     self::$_resovedAttendeeCache[$attender->user_type][$attender->user_id] = $attendeeTypeSet[$idx];
770                     
771                     $attender->user_id = $attendeeTypeSet[$idx];
772                 }
773             }
774         }
775         
776         
777         foreach ($eventAttendee as $idx => $attendee) {
778             $event = is_array($events) && (isset($events[$idx]) || array_key_exists($idx, $events)) ? $events[$idx] : NULL;
779             
780             foreach ($attendee as $attender) {
781                 // keep authkey if user has editGrant to displaycontainer
782                 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]) {
783                     continue;
784                 }
785                 
786                 // keep authkey if attender is a contact (no account) and user has editGrant for event
787                 if ($attender->user_type == self::USERTYPE_USER
788                     && $attender->user_id instanceof Tinebase_Record_Abstract
789                     && (!$attender->user_id->has('account_id') || !$attender->user_id->account_id)
790                     && (!$event || $event->{Tinebase_Model_Grants::GRANT_EDIT})
791                 ) {
792                     continue;
793                 }
794                 
795                 $attender->status_authkey = NULL;
796             }
797         }
798     }
799     
800     /**
801      * checks if given alarm should be send to given attendee
802      * 
803      * @param  Calendar_Model_Attender $_attendee
804      * @param  Tinebase_Model_Alarm    $_alarm
805      * @return bool
806      */
807     public static function isAlarmForAttendee($_attendee, $_alarm, $_event=NULL)
808     {
809         // attendee: array with one user_type/id if alarm is for one attendee only
810         $attendeeOption = $_alarm->getOption('attendee');
811         
812         // skip: array of array of user_type/id with attendees this alarm is to skip for
813         $skipOption = $_alarm->getOption('skip');
814         
815         if ($attendeeOption) {
816             return (bool) self::getAttendee(new Tinebase_Record_RecordSet('Calendar_Model_Attender', array($_attendee)), new Calendar_Model_Attender($attendeeOption));
817         }
818         
819         if (is_array($skipOption)) {
820             $skipAttendees = new Tinebase_Record_RecordSet('Calendar_Model_Attender', $skipOption);
821             if(self::getAttendee($skipAttendees, $_attendee)) {
822                 return false;
823             }
824         }
825         
826         $isOrganizerCondition = $_event ? $_event->isOrganizer($_attendee) : TRUE;
827         $isAttendeeCondition = $_event && $_event->attendee instanceof Tinebase_Record_RecordSet ? self::getAttendee($_event->attendee, $_attendee) : TRUE;
828         return ($isAttendeeCondition || $isOrganizerCondition)&& $_attendee->status != Calendar_Model_Attender::STATUS_DECLINED;
829     }
830 }