fixes/improves some more tests for ldap backend
[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 $_resolvedAttendeesCache = array(
48         self::USERTYPE_USER        => array(),
49         self::USERTYPE_GROUPMEMBER => array(),
50         self::USERTYPE_GROUP       => array(),
51         self::USERTYPE_LIST        => array(),
52         self::USERTYPE_RESOURCE    => array(),
53         Calendar_Model_AttenderFilter::USERTYPE_MEMBEROF => array()
54     );
55     
56     /**
57      * key in $_validators/$_properties array for the filed which 
58      * represents the identifier
59      * 
60      * @var string
61      */
62     protected $_identifier = 'id';
63     
64     /**
65      * application the record belongs to
66      *
67      * @var string
68      */
69     protected $_application = 'Calendar';
70     
71     /**
72      * validators
73      *
74      * @var array
75      */
76     protected $_validators = array(
77         // tine record fields
78         'id'                   => array('allowEmpty' => true,  'Alnum'),
79         'created_by'           => array('allowEmpty' => true          ),
80         'creation_time'        => array('allowEmpty' => true          ),
81         'last_modified_by'     => array('allowEmpty' => true          ),
82         'last_modified_time'   => array('allowEmpty' => true          ),
83         'is_deleted'           => array('allowEmpty' => true          ),
84         'deleted_time'         => array('allowEmpty' => true          ),
85         'deleted_by'           => array('allowEmpty' => true          ),
86         'seq'                  => array('allowEmpty' => true,  'Int'  ),
87         
88         'cal_event_id'         => array('allowEmpty' => true/*,  'Alnum'*/),
89         'user_id'              => array('allowEmpty' => false,        ),
90         'user_type'            => array(
91             'allowEmpty' => true,
92             array('InArray', array(self::USERTYPE_USER, self::USERTYPE_GROUP, self::USERTYPE_GROUPMEMBER, self::USERTYPE_RESOURCE))
93         ),
94         'role'                 => array('allowEmpty' => true          ),
95         'quantity'             => array('allowEmpty' => true, 'Int'   ),
96         'status'               => array('allowEmpty' => true          ),
97         'status_authkey'       => array('allowEmpty' => true, 'Alnum' ),
98         'displaycontainer_id'  => array('allowEmpty' => true, 'Int'   ),
99         'transp'               => array(
100             'allowEmpty' => true,
101             array('InArray', array(Calendar_Model_Event::TRANSP_TRANSP, Calendar_Model_Event::TRANSP_OPAQUE))
102         ),
103     );
104
105     /**
106      * datetime fields
107      *
108      * @var array
109      */
110     protected $_datetimeFields = array(
111         'creation_time',
112         'last_modified_time',
113         'deleted_time',
114     );
115
116     /**
117      * returns accountId of this attender if present
118      * 
119      * @return string
120      */
121     public function getUserAccountId()
122     {
123         if (! in_array($this->user_type, array(self::USERTYPE_USER, self::USERTYPE_GROUPMEMBER))) {
124             return NULL;
125         }
126         
127         try {
128             $contact = Addressbook_Controller_Contact::getInstance()->get($this->user_id, null, false);
129             return $contact->account_id ? $contact->account_id : NULL;
130         } catch (Exception $e) {
131             return NULL;
132         }
133     }
134     
135     /**
136      * get email of attender if exists
137      * 
138      * @return string
139      */
140     public function getEmail()
141     {
142         $resolvedUser = $this->getResolvedUser();
143         if (! $resolvedUser instanceof Tinebase_Record_Abstract) {
144             return '';
145         }
146         
147         switch ($this->user_type) {
148             case self::USERTYPE_USER:
149             case self::USERTYPE_GROUPMEMBER:
150                 return $resolvedUser->getPreferedEmailAddress();
151                 break;
152             case self::USERTYPE_GROUP:
153                 return $resolvedUser->getId();
154                 break;
155             case self::USERTYPE_RESOURCE:
156                 return $resolvedUser->email;
157                 break;
158             default:
159                 throw new Exception("type " . $this->user_type . " not yet supported");
160                 break;
161         }
162     }
163
164     /**
165      * get email addresses this attendee had in the past
166      *
167      * @return array
168      */
169     public function getEmailsFromHistory()
170     {
171         $emails = array();
172
173         $typeMap = array(
174             self::USERTYPE_USER        => 'Addressbook_Model_Contact',
175             self::USERTYPE_GROUPMEMBER => 'Addressbook_Model_Contact',
176             self::USERTYPE_RESOURCE    => 'Calendar_Model_Resource',
177         );
178
179         if (isset ($typeMap[$this->user_type])) {
180             $type = $typeMap[$this->user_type];
181             $id = $this->user_id instanceof Tinebase_Record_Abstract ? $this->user_id->getId() : $this->user_id;
182
183             $modifications = Tinebase_Timemachine_ModificationLog::getInstance()->getModifications(
184                 Tinebase_Helper::array_value(0, explode('_', $type)),
185                 $this->user_id instanceof Tinebase_Record_Abstract ? $this->user_id->getId() : $this->user_id,
186                 $type,
187                 'Sql',
188                 $this->creation_time
189             );
190
191             foreach($modifications as $modification) {
192                 if (in_array($modification->modified_attribute, array('email', 'email_home'))) {
193                     if ($modification->old_value) {
194                         $emails[] = $modification->old_value;
195                     }
196                 }
197             }
198         }
199
200         return $emails;
201     }
202
203     /**
204      * get name of attender
205      * 
206      * @return string
207      */
208     public function getName()
209     {
210         $resolvedUser = $this->getResolvedUser();
211         if (! $resolvedUser instanceof Tinebase_Record_Abstract) {
212             Tinebase_Translation::getTranslation('Calendar');
213             return Tinebase_Translation::getTranslation('Calendar')->_('unknown');
214         }
215         
216         switch ($this->user_type) {
217             case self::USERTYPE_USER:
218             case self::USERTYPE_GROUPMEMBER:
219                 return $resolvedUser->n_fileas;
220                 break;
221             case self::USERTYPE_GROUP:
222             case self::USERTYPE_RESOURCE:
223                 return $resolvedUser->name ?: $resolvedUser->n_fileas;
224                 break;
225             default:
226                 throw new Exception("type " . $this->user_type . " not yet supported");
227                 break;
228         }
229     }
230     
231     /**
232      * returns the resolved user_id
233      * 
234      * @return Tinebase_Record_Abstract
235      */
236     public function getResolvedUser()
237     {
238         $clone = clone $this;
239         $resolvable = new Tinebase_Record_RecordSet('Calendar_Model_Attender', array($clone));
240         self::resolveAttendee($resolvable);
241         
242         if ($this->user_type === self::USERTYPE_RESOURCE) {
243             $resource = $clone->user_id;
244             // return pseudo contact with resource data
245             $result = new Addressbook_Model_Contact(array(
246                 'n_family'  => $resource->name,
247                 'email'     => $resource->email,
248                 'id'        => $resource->getId(),
249             ));
250         } else {
251             $result = $clone->user_id;
252         }
253         
254         return $result;
255     }
256     
257     public function getStatusString()
258     {
259         $statusConfig = Calendar_Config::getInstance()->attendeeStatus;
260         $statusRecord = $statusConfig && $statusConfig->records instanceof Tinebase_Record_RecordSet ? $statusConfig->records->getById($this->status) : false;
261         
262         return $statusRecord ? $statusRecord->value : $this->status;
263     }
264     
265     public function getRoleString()
266     {
267         $rolesConfig = Calendar_Config::getInstance()->attendeeRoles;
268         $rolesRecord = $rolesConfig && $rolesConfig->records instanceof Tinebase_Record_RecordSet ? $rolesConfig->records->getById($this->role) : false;
269         
270         return $rolesRecord? $rolesRecord->value : $this->role;
271     }
272
273     /**
274      * returns if given attendee is the same as this attendee
275      *
276      * @param  Calendar_Model_Attender $compareTo
277      * @return bool
278      */
279     public function isSame($compareTo)
280     {
281         $compareToSet = new Tinebase_Record_RecordSet('Calendar_Model_Attender', array($compareTo));
282         return !!self::getAttendee($compareToSet, $this);
283     }
284
285     /**
286      * sets the record related properties from user generated input.
287      * 
288      * Input-filtering and validation by Zend_Filter_Input can enabled and disabled
289      *
290      * @param array $_data            the new data to set
291      * @throws Tinebase_Exception_Record_Validation when content contains invalid or missing data
292      */
293     public function setFromArray(array $_data)
294     {
295         if (isset($_data['displaycontainer_id']) && is_array($_data['displaycontainer_id'])) {
296             $_data['displaycontainer_id'] = $_data['displaycontainer_id']['id'];
297         }
298         
299         if (isset($_data['user_id']) && is_array($_data['user_id'])) {
300             if ((isset($_data['user_id']['accountId']) || array_key_exists('accountId', $_data['user_id']))) {
301                 // 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
302                 $_data['user_id'] = Addressbook_Controller_Contact::getInstance()->getContactByUserId($_data['user_id']['accountId'], TRUE)->getId();
303             } elseif ((isset($_data['user_id']['group_id']) || array_key_exists('group_id', $_data['user_id']))) {
304                 $_data['user_id'] = is_array($_data['user_id']['group_id']) ? $_data['user_id']['group_id'][0] : $_data['user_id']['group_id'];
305             } else if ((isset($_data['user_id']['id']) || array_key_exists('id', $_data['user_id']))) {
306                 $_data['user_id'] = $_data['user_id']['id'];
307             }
308         }
309         
310         if (empty($_data['quantity'])) {
311             $_data['quantity'] = 1;
312         }
313         
314         parent::setFromArray($_data);
315     }
316     
317     /**
318      * converts an array of emails to a recordSet of attendee for given record
319      * 
320      * @param  Calendar_Model_Event $_event
321      * @param  array                $_emails
322      * @param  bool                 $_implicitAddMissingContacts
323      */
324     public static function emailsToAttendee(Calendar_Model_Event $_event, $_emails, $_implicitAddMissingContacts = TRUE)
325     {
326         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
327             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " list of new attendees " . print_r($_emails, true));
328         
329         if (! $_event->attendee instanceof Tinebase_Record_RecordSet) {
330             $_event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
331         }
332                 
333         // resolve current attendee
334         self::resolveAttendee($_event->attendee);
335         
336         // build currentMailMap
337         // NOTE: non resolvable attendee will be discarded in the map
338         //       this is _important_ for the calculation of migration as it
339         //       saves us from deleting attendee out of current users scope
340         $emailsOfCurrentAttendees = array();
341         foreach ($_event->attendee as $currentAttendee) {
342             if ($currentAttendeeEmailAddress = $currentAttendee->getEmail()) {
343                 $emailsOfCurrentAttendees[$currentAttendeeEmailAddress] = $currentAttendee;
344             }
345         }
346         
347         // collect emails of new attendees (skipping if no email present)
348         $emailsOfNewAttendees = array();
349         foreach ($_emails as $newAttendee) {
350             if ($newAttendee['email']) {
351                 $emailsOfNewAttendees[$newAttendee['email']] = $newAttendee;
352             }
353         }
354         
355         // attendees to remove
356         $attendeesToDelete = array_diff_key($emailsOfCurrentAttendees, $emailsOfNewAttendees);
357         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " attendees to delete " . print_r(array_keys($attendeesToDelete), true));
358         
359         // delete attendees no longer attending from recordset
360         foreach ($attendeesToDelete as $attendeeToDelete) {
361             // NOTE: email of attendee might have changed in the meantime
362             //       => get old email adresses from modlog and try to match
363             foreach($attendeeToDelete->getEmailsFromHistory() as $oldEmail) {
364                 if (isset($emailsOfNewAttendees[$oldEmail])) {
365                     unset($emailsOfNewAttendees[$oldEmail]);
366                     continue 2;
367                 }
368             }
369             
370             $_event->attendee->removeRecord($attendeeToDelete);
371         }
372         
373         // attendees to keep and update
374         $attendeesToKeep   = array_diff_key($emailsOfCurrentAttendees, $attendeesToDelete);
375         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " attendees to keep " . print_r(array_keys($attendeesToKeep), true));
376         //var_dump($attendeesToKeep);
377         foreach($attendeesToKeep as $emailAddress => $attendeeToKeep) {
378             $newSettings = $emailsOfNewAttendees[$emailAddress];
379
380             // update object by reference
381             $attendeeToKeep->status = isset($newSettings['partStat']) ? $newSettings['partStat'] : $attendeeToKeep->status;
382             $attendeeToKeep->role   = $newSettings['role'];
383         }
384
385         // new attendess to add to event
386         $attendeesToAdd    = array_diff_key($emailsOfNewAttendees,     $emailsOfCurrentAttendees);
387         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " attendees to add " . print_r(array_keys($attendeesToAdd), true));
388         
389         $smtpConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::SMTP, new Tinebase_Config_Struct())->toArray();
390         
391         // add attendee identified by their emailAdress
392         foreach ($attendeesToAdd as $newAttendee) {
393             $attendeeId = NULL;
394             
395             if ($newAttendee['userType'] == Calendar_Model_Attender::USERTYPE_USER) {
396                 // does a contact with this email address exist?
397                 if ($contact = self::resolveEmailToContact($newAttendee, false)) {
398                     $attendeeId = $contact->getId();
399                     
400                 }
401                 
402                 // does a resouce with this email address exist?
403                 if ( ! $attendeeId) {
404                     $resources = Calendar_Controller_Resource::getInstance()->search(new Calendar_Model_ResourceFilter(array(
405                         array('field' => 'email', 'operator' => 'equals', 'value' => $newAttendee['email']),
406                     )));
407                     
408                     if(count($resources) > 0) {
409                         $newAttendee['userType'] = Calendar_Model_Attender::USERTYPE_RESOURCE;
410                         $attendeeId = $resources->getFirstRecord()->getId();
411                     }
412                 }
413                 // does a list with this name exist?
414                 if ( ! $attendeeId &&
415                     isset($smtpConfig['primarydomain']) && 
416                     preg_match('/(?P<localName>.*)@' . preg_quote($smtpConfig['primarydomain']) . '$/', $newAttendee['email'], $matches)
417                 ) {
418                     $lists = Addressbook_Controller_List::getInstance()->search(new Addressbook_Model_ListFilter(array(
419                         array('field' => 'name',       'operator' => 'equals', 'value' => $matches['localName']),
420                         array('field' => 'type',       'operator' => 'equals', 'value' => Addressbook_Model_List::LISTTYPE_GROUP),
421                         array('field' => 'showHidden', 'operator' => 'equals', 'value' => TRUE),
422                     )));
423                     
424                     if(count($lists) > 0) {
425                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
426                             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " found # of lists " . count($lists));
427                     
428                         $newAttendee['userType'] = Calendar_Model_Attender::USERTYPE_GROUP;
429                         $attendeeId = $lists->getFirstRecord()->group_id;
430                     }
431                 } 
432                 
433                 if (! $attendeeId) {
434                     // autocreate a contact if allowed
435                     $contact = self::resolveEmailToContact($newAttendee, $_implicitAddMissingContacts);
436                     if ($contact) {
437                         $attendeeId = $contact->getId();
438                     }
439                 }
440             } else if($newAttendee['userType'] == Calendar_Model_Attender::USERTYPE_GROUP) {
441                 $lists = Addressbook_Controller_List::getInstance()->search(new Addressbook_Model_ListFilter(array(
442                     array('field' => 'name',       'operator' => 'equals', 'value' => $newAttendee['displayName']),
443                     array('field' => 'type',       'operator' => 'equals', 'value' => Addressbook_Model_List::LISTTYPE_GROUP),
444                     array('field' => 'showHidden', 'operator' => 'equals', 'value' => TRUE),
445                 )));
446                 
447                 if(count($lists) > 0) {
448                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " found # of lists " . count($lists));
449                 
450                     $attendeeId = $lists->getFirstRecord()->group_id;
451                 }
452             }
453             
454             if ($attendeeId !== NULL) {
455                 // finally add to attendee
456                 $_event->attendee->addRecord(new Calendar_Model_Attender(array(
457                     'user_id'   => $attendeeId,
458                     'user_type' => $newAttendee['userType'],
459                     'status'    => isset($newAttendee['partStat']) ? $newAttendee['partStat'] : self::STATUS_NEEDSACTION,
460                     'role'      => $newAttendee['role']
461                 )));
462             }
463         }
464         
465         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
466             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " updated attendees list " . print_r($_event->attendee->toArray(), true));
467     }
468     
469     /**
470      * get attendee with user_id = email address and create contacts for them on the fly if they do not exist
471      * 
472      * @param Calendar_Model_Event $_event
473      * @throws Tinebase_Exception_InvalidArgument
474      */
475     public static function resolveEmailOnlyAttendee(Calendar_Model_Event $_event)
476     {
477         if (! $_event->attendee instanceof Tinebase_Record_RecordSet) {
478             $_event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
479         }
480         
481         foreach ($_event->attendee as $currentAttendee) {
482             if (is_string($currentAttendee->user_id) && preg_match(Tinebase_Mail::EMAIL_ADDRESS_REGEXP, $currentAttendee->user_id)) {
483                 if ($currentAttendee->user_type !== Calendar_Model_Attender::USERTYPE_USER) {
484                     throw new Tinebase_Exception_InvalidArgument('it is only allowed to set contacts as email only attender');
485                 }
486                 $contact = self::resolveEmailToContact(array(
487                     'email'     => $currentAttendee->user_id,
488                 ));
489                 $currentAttendee->user_id = $contact->getId();
490             }
491         }
492     }
493     
494    /**
495     * check if contact with given email exists in addressbook and creates it if not
496     *
497     * @param  array $_attenderData array with email, firstname and lastname (if available)
498     * @param  boolean $_implicitAddMissingContacts
499     * @return Addressbook_Model_Contact
500     * 
501     * @todo filter by fn if multiple matches
502     */
503     public static function resolveEmailToContact($_attenderData, $_implicitAddMissingContacts = TRUE)
504     {
505         if (! isset($_attenderData['email']) || empty($_attenderData['email'])) {
506             throw new Tinebase_Exception_InvalidArgument('email address is needed to resolve contact');
507         }
508         
509         $email = $_attenderData['email'];
510         
511         $contacts = Addressbook_Controller_Contact::getInstance()->search(new Addressbook_Model_ContactFilter(array(
512             array('condition' => 'OR', 'filters' => array(
513                 array('field' => 'email',      'operator'  => 'equals', 'value' => $email),
514                 array('field' => 'email_home', 'operator'  => 'equals', 'value' => $email)
515             )),
516         )), new Tinebase_Model_Pagination(array(
517             'sort'    => 'type', // prefer user over contact
518             'dir'     => 'DESC',
519             'limit'   => 1
520         )));
521         
522         if (count($contacts) > 0) {
523             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
524                     . " Found # of contacts " . count($contacts));
525             $result = $contacts->getFirstRecord();
526         
527         } else if ($_implicitAddMissingContacts === TRUE) {
528             $translation = Tinebase_Translation::getTranslation('Calendar');
529             $i18nNote = $translation->_('This contact has been automatically added by the system as an event attender');
530             if ($email !== $_attenderData['email']) {
531                 $i18nNote .= "\n";
532                 $i18nNote .= $translation->_('The email address has been shortened:') . ' ' . $_attenderData['email'] . ' -> ' . $email;
533             }
534             $contactData = array(
535                 'note'        => $i18nNote,
536                 'email'       => $email,
537                 'n_family'    => (isset($_attenderData['lastName']) && ! empty($_attenderData['lastName'])) ? $_attenderData['lastName'] : $email,
538                 'n_given'     => (isset($_attenderData['firstName'])) ? $_attenderData['firstName'] : '',
539             );
540             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
541                     . " Ádd new contact " . print_r($contactData, true));
542             $contact = new Addressbook_Model_Contact($contactData);
543             $result = Addressbook_Controller_Contact::getInstance()->create($contact, FALSE);
544         } else {
545             $result = NULL;
546         }
547         
548         return $result;
549     }
550     
551     /**
552      * resolves group members and adds/removes them if nesesary
553      * 
554      * NOTE: If a user is listed as user and as groupmember, we suppress the groupmember
555      * 
556      * NOTE: The role to assign to a new group member is not always clear, as multiple groups
557      *       might be the 'source' of the group member. To deal with this, we take the role of
558      *       the first group when we add new group members
559      *       
560      * @param Tinebase_Record_RecordSet $_attendee
561      * @return void
562      */
563     public static function resolveGroupMembers($_attendee)
564     {
565         if (! $_attendee instanceof Tinebase_Record_RecordSet) {
566             return;
567         }
568         $_attendee->addIndices(array('user_type'));
569         
570         // flatten user_ids (not groups for group/list handling bellow)
571         foreach($_attendee as $attendee) {
572             if ($attendee->user_type != Calendar_Model_Attender::USERTYPE_GROUP && $attendee->user_id instanceof Tinebase_Record_Abstract) {
573                 $attendee->user_id = $attendee->user_id->getId();
574             }
575         }
576         
577         $groupAttendee = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUP);
578         
579         $allCurrGroupMembers = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
580         $allCurrGroupMembersContactIds = $allCurrGroupMembers->user_id;
581         
582         $allGroupMembersContactIds = array();
583         foreach ($groupAttendee as $groupAttender) {
584             $listId = null;
585         
586             if ($groupAttender->user_id instanceof Addressbook_Model_List) {
587                 $listId = $groupAttender->user_id->getId();
588             } else if ($groupAttender->user_id !== NULL) {
589                 $group = Tinebase_Group::getInstance()->getGroupById($groupAttender->user_id);
590                 if (!empty($group->list_id)) {
591                     $listId = $group->list_id;
592                 }
593             } else {
594                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
595                     . ' Group attender ID missing');
596                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
597                     . ' ' . print_r($groupAttender->toArray(), TRUE));
598             }
599             
600             if ($listId !== null) {
601                 $groupAttenderContactIds = Addressbook_Controller_List::getInstance()->get($listId)->members;
602                 $allGroupMembersContactIds = array_merge($allGroupMembersContactIds, $groupAttenderContactIds);
603                 
604                 $toAdd = array_diff($groupAttenderContactIds, $allCurrGroupMembersContactIds);
605                 
606                 foreach($toAdd as $userId) {
607                     $_attendee->addRecord(new Calendar_Model_Attender(array(
608                         'user_type' => Calendar_Model_Attender::USERTYPE_GROUPMEMBER,
609                         'user_id'   => $userId,
610                         'role'      => $groupAttender->role
611                     )));
612                 }
613             }
614         }
615         
616         $toDel = array_diff($allCurrGroupMembersContactIds, $allGroupMembersContactIds);
617         foreach ($toDel as $idx => $contactId) {
618             $attender = $allCurrGroupMembers->find('user_id', $contactId);
619             $_attendee->removeRecord($attender);
620         }
621         
622         // calculate double members (groupmember + user)
623         $groupmembers = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_GROUPMEMBER);
624         $users        = $_attendee->filter('user_type', Calendar_Model_Attender::USERTYPE_USER);
625         $doublicates = array_intersect($users->user_id, $groupmembers->user_id);
626         foreach ($doublicates as $user_id) {
627             $attender = $groupmembers->find('user_id', $user_id);
628             $_attendee->removeRecord($attender);
629         }
630     }
631     
632     /**
633      * get own attender
634      * 
635      * @param Tinebase_Record_RecordSet $_attendee
636      * @return Calendar_Model_Attender|NULL
637      */
638     public static function getOwnAttender($_attendee)
639     {
640         return self::getAttendee($_attendee, new Calendar_Model_Attender(array(
641             'user_id'   => Tinebase_Core::getUser()->contact_id,
642             'user_type' => Calendar_Model_Attender::USERTYPE_USER
643         )));
644     }
645     
646     /**
647      * get a single attendee from set of attendee
648      * 
649      * @param Tinebase_Record_RecordSet $_attendeesSet
650      * @param Calendar_Model_Attender   $_attendee
651      * @return Calendar_Model_Attender|NULL
652      */
653     public static function getAttendee($_attendeesSet, Calendar_Model_Attender $_attendee)
654     {
655         if (!$_attendeesSet instanceof Tinebase_Record_RecordSet) {
656             return null;
657         }
658         
659         $attendeeUserId = $_attendee->user_id instanceof Tinebase_Record_Abstract
660             ? $_attendee->user_id->getId()
661             : $_attendee->user_id;
662         
663         foreach ($_attendeesSet as $attendeeFromSet) {
664             $attendeeFromSetUserId = $attendeeFromSet->user_id instanceof Tinebase_Record_Abstract 
665                 ? $attendeeFromSet->user_id->getId()
666                 : $attendeeFromSet->user_id;
667             
668             if ($attendeeFromSetUserId === $attendeeUserId) {
669                 if ($attendeeFromSet->user_type === $_attendee->user_type) {
670                     // can stop here
671                     return $attendeeFromSet;
672                 }
673                 
674                 if (   $_attendee->user_type       === Calendar_Model_Attender::USERTYPE_USER
675                     && $attendeeFromSet->user_type === Calendar_Model_Attender::USERTYPE_GROUPMEMBER
676                 ) {
677                     $foundGroupMember = $attendeeFromSet;
678                     // continue searching for $_attendee->user_type
679                     // @todo maybe we can also return in this case immediately
680                 }
681             }
682         }
683         
684         return isset($foundGroupMember) ? $foundGroupMember : null;
685     }
686     
687     /**
688      * returns migration of two attendee sets
689      * 
690      * @param  Tinebase_Record_RecordSet $_current
691      * @param  Tinebase_Record_RecordSet $_update
692      * @return array migrationKey => Tinebase_Record_RecordSet
693      */
694     public static function getMigration($_current, $_update)
695     {
696         $result = array(
697             'toDelete' => new Tinebase_Record_RecordSet('Calendar_Model_Attender'),
698             'toCreate' => clone $_update,
699             'toUpdate' => new Tinebase_Record_RecordSet('Calendar_Model_Attender'),
700         );
701         
702         foreach($_current as $currAttendee) {
703             $updateAttendee = self::getAttendee($result['toCreate'], $currAttendee);
704             if ($updateAttendee) {
705                 $result['toUpdate']->addRecord($updateAttendee);
706                 $result['toCreate']->removeRecord($updateAttendee);
707             } else {
708                 $result['toDelete']->addRecord($currAttendee);
709             }
710         }
711         
712         return $result;
713     }
714     
715     /**
716      * fill resolved attendees class cache
717      * 
718      * @param  Tinebase_Record_RecordSet|array  $eventAttendees
719      * @throws Calendar_Exception
720      */
721     public static function fillResolvedAttendeesCache($eventAttendees)
722     {
723         if (empty($eventAttendees)) {
724             return;
725         }
726         
727         $eventAttendees = $eventAttendees instanceof Tinebase_Record_RecordSet
728             ? array($eventAttendees)
729             : $eventAttendees;
730         
731         $typeMap = array(
732             self::USERTYPE_USER        => array(),
733             self::USERTYPE_GROUPMEMBER => array(),
734             self::USERTYPE_GROUP       => array(),
735             self::USERTYPE_LIST        => array(),
736             self::USERTYPE_RESOURCE    => array(),
737             Calendar_Model_AttenderFilter::USERTYPE_MEMBEROF => array()
738         );
739         
740         // build type map 
741         foreach ($eventAttendees as $eventAttendee) {
742             foreach ($eventAttendee as $attendee) {
743                 $user     = $attendee->user_id;
744                 $userType = $attendee->user_type;
745                 $userId   = $user instanceof Tinebase_Record_Abstract
746                     ? $user->getId()
747                     : $user;
748                 
749                 if (isset(self::$_resolvedAttendeesCache[$userType][$userId])) {
750                     // already in cache
751                     continue;
752                 }
753                 
754                 if ($user instanceof Tinebase_Record_Abstract) {
755                     // can fill cache with model from $attendee
756                     self::$_resolvedAttendeesCache[$userType][$userId] = $user;
757                     
758                     continue;
759                 }
760                 
761                 // must be resolved
762                 $typeMap[$userType][] = $userId;
763             }
764         }
765         
766         // get all missing user_id entries
767         foreach ($typeMap as $type => $ids) {
768             $ids = array_unique($ids);
769             
770             if (empty($ids)) {
771                 continue;
772             }
773             
774             switch ($type) {
775                 case self::USERTYPE_USER:
776                 case self::USERTYPE_GROUPMEMBER:
777                     $resolveCf = Addressbook_Controller_Contact::getInstance()->resolveCustomfields(FALSE);
778                     $contacts  = Addressbook_Controller_Contact::getInstance()->getMultiple($ids, TRUE);
779                     Addressbook_Controller_Contact::getInstance()->resolveCustomfields($resolveCf);
780                     
781                     foreach ($contacts as $contact) {
782                         self::$_resolvedAttendeesCache[$type][$contact->getId()] = $contact;
783                     }
784                     
785                     break;
786                     
787                 case self::USERTYPE_GROUP:
788                 case Calendar_Model_AttenderFilter::USERTYPE_MEMBEROF:
789                     // first fetch the groups, then the lists identified by list_id
790                     $groups = Tinebase_Group::getInstance()->getMultiple($ids);
791                     $lists  = Addressbook_Controller_List::getInstance()->getMultiple($groups->list_id, true);
792                     
793                     foreach ($groups as $group) {
794                         $list = $lists->getById($group->list_id);
795                         if ($list) {
796                             self::$_resolvedAttendeesCache[$type][$group->getId()] = $list;
797                         }
798                     }
799                     
800                     break;
801                     
802                 case self::USERTYPE_RESOURCE:
803                     $resources = Calendar_Controller_Resource::getInstance()->getMultiple($ids, true);
804                     
805                     foreach ($resources as $resource) {
806                         self::$_resolvedAttendeesCache[$type][$resource->getId()] = $resource;
807                     }
808                     
809                     break;
810                     
811                 default:
812                     throw new Calendar_Exception("type $type not supported");
813                     
814                     break;
815             }
816         }
817     }
818     
819     /**
820      * return list of resolved attendee for given record(set)
821      * 
822      * @param Tinebase_Record_RecordSet|array   $eventAttendees 
823      * @param bool                              $resolveDisplayContainers
824      * @return Tinebase_Record_RecordSet
825      */
826     public static function getResolvedAttendees($eventAttendees, $resolveDisplayContainers = TRUE)
827     {
828         if (empty($eventAttendees)) {
829             return null;
830         }
831         
832         self::fillResolvedAttendeesCache($eventAttendees);
833         
834         $eventAttendees = $eventAttendees instanceof Tinebase_Record_RecordSet
835             ? array($eventAttendees)
836             : $eventAttendees;
837         
838         $foundDisplayContainers = false;
839         
840         // set containing all attendee
841         $allAttendees = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
842         
843         // build type map 
844         foreach ($eventAttendees as $eventAttendee) {
845             foreach ($eventAttendee as $attendee) {
846                 if (   $resolveDisplayContainers
847                     && ! $foundDisplayContainers
848                     && (is_int($attendee->displaycontainer_id) || is_string($attendee->displaycontainer_id))
849                 ) {
850                         $foundDisplayContainers = true;
851                 }
852                 
853                 if ($attendee->user_id instanceof Tinebase_Record_Abstract) {
854                     // already resolved
855                     $allAttendees->addRecord($attendee);
856                     
857                     continue;
858                 }
859                 
860                 if (isset(self::$_resolvedAttendeesCache[$attendee->user_type][$attendee->user_id])) {
861                     $clonedAttendee = clone $attendee;
862                     
863                     // resolveable from cache
864                     $clonedAttendee->user_id = self::$_resolvedAttendeesCache[$attendee->user_type][$attendee->user_id];
865                     
866                     $allAttendees->addRecord($clonedAttendee);
867                     
868                     continue;
869                 }
870                 
871                 // not resolved => problem!!!
872             }
873         }
874         
875         // resolve display containers
876         if ($resolveDisplayContainers && $foundDisplayContainers) {
877             Tinebase_Container::getInstance()->getGrantsOfRecords($allAttendees, Tinebase_Core::getUser(), 'displaycontainer_id');
878         }
879         
880         return $allAttendees;
881     }
882     
883     /**
884      * resolves given attendee for json representation
885      * 
886      * @todo move status_authkey cleanup elsewhere
887      * @todo use self::getResolvedAttendees to avoid code duplication
888      * 
889      * @param Tinebase_Record_RecordSet|array   $eventAttendees 
890      * @param bool                              $resolveDisplayContainers
891      * @param Calendar_Model_Event|array        $_events
892      */
893     public static function resolveAttendee($eventAttendees, $resolveDisplayContainers = TRUE, $_events = NULL)
894     {
895         if (empty($eventAttendees)) {
896             return;
897         }
898         
899         $eventAttendee = $eventAttendees instanceof Tinebase_Record_RecordSet ? array($eventAttendees) : $eventAttendees;
900         $events = $_events instanceof Tinebase_Record_Abstract ? array($_events) : $_events;
901         
902         // set containing all attendee
903         $allAttendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
904         $typeMap = array();
905         
906         // build type map 
907         foreach ($eventAttendee as $attendee) {
908             foreach ($attendee as $attender) {
909                 $allAttendee->addRecord($attender);
910                 
911                 if ($attender->user_id instanceof Tinebase_Record_Abstract) {
912                     // already resolved
913                     continue;
914                 } elseif ((isset(self::$_resolvedAttendeesCache[$attender->user_type]) || array_key_exists($attender->user_type, self::$_resolvedAttendeesCache)) && (isset(self::$_resolvedAttendeesCache[$attender->user_type][$attender->user_id]) || array_key_exists($attender->user_id, self::$_resolvedAttendeesCache[$attender->user_type]))){
915                     // already in cache
916                     $attender->user_id = self::$_resolvedAttendeesCache[$attender->user_type][$attender->user_id];
917                 } else {
918                     if (! (isset($typeMap[$attender->user_type]) || array_key_exists($attender->user_type, $typeMap))) {
919                         $typeMap[$attender->user_type] = array();
920                     }
921                     $typeMap[$attender->user_type][] = $attender->user_id;
922                 }
923             }
924         }
925         
926         // resolve display containers
927         if ($resolveDisplayContainers) {
928             $displaycontainerIds = array_diff($allAttendee->displaycontainer_id, array(''));
929             if (! empty($displaycontainerIds)) {
930                 Tinebase_Container::getInstance()->getGrantsOfRecords($allAttendee, Tinebase_Core::getUser(), 'displaycontainer_id');
931             }
932         }
933         
934         // get all user_id entries
935         foreach ($typeMap as $type => $ids) {
936             switch ($type) {
937                 case self::USERTYPE_USER:
938                 case self::USERTYPE_GROUPMEMBER:
939                     $resolveCf = Addressbook_Controller_Contact::getInstance()->resolveCustomfields(FALSE);
940                     $typeMap[$type] = Addressbook_Controller_Contact::getInstance()->getMultiple(array_unique($ids), TRUE);
941                     Addressbook_Controller_Contact::getInstance()->resolveCustomfields($resolveCf);
942                     break;
943                 case self::USERTYPE_GROUP:
944                 case Calendar_Model_AttenderFilter::USERTYPE_MEMBEROF:
945                     // first fetch the groups, then the lists identified by list_id
946                     $typeMap[$type] = Tinebase_Group::getInstance()->getMultiple(array_unique($ids));
947                     $typeMap[self::USERTYPE_LIST] = Addressbook_Controller_List::getInstance()->getMultiple($typeMap[$type]->list_id, true);
948                     break;
949                 case self::USERTYPE_RESOURCE:
950                     $typeMap[$type] = Calendar_Controller_Resource::getInstance()->getMultiple(array_unique($ids), true);
951                     break;
952                 default:
953                     throw new Exception("type $type not supported");
954                     break;
955             }
956         }
957         
958         // sort entries in
959         foreach ($eventAttendee as $attendee) {
960             foreach ($attendee as $attender) {
961                 if ($attender->user_id instanceof Tinebase_Record_Abstract) {
962                     // allready resolved from cache
963                     continue;
964                 }
965                 
966                 if ($attender->user_type == self::USERTYPE_GROUP) {
967                     $attendeeTypeSet = $typeMap[$attender->user_type];
968                     $idx = $attendeeTypeSet->getIndexById($attender->user_id);
969                     if ($idx !== false) {
970                         $group = $attendeeTypeSet[$idx];
971                         $attendeeTypeSet = $typeMap[self::USERTYPE_LIST];
972                         $idx = $attendeeTypeSet->getIndexById($group->list_id);
973                     }
974                 } else {
975                     $attendeeTypeSet = $typeMap[$attender->user_type];
976                     $idx = $attendeeTypeSet->getIndexById($attender->user_id);
977                 }
978                 
979                 if ($idx !== false) {
980                     // copy to cache
981                     self::$_resolvedAttendeesCache[$attender->user_type][$attender->user_id] = $attendeeTypeSet[$idx];
982                     
983                     $attender->user_id = $attendeeTypeSet[$idx];
984                 }
985             }
986         }
987         
988         
989         foreach ($eventAttendee as $idx => $attendee) {
990             $event = is_array($events) && (isset($events[$idx]) || array_key_exists($idx, $events)) ? $events[$idx] : NULL;
991             
992             foreach ($attendee as $attender) {
993                 // keep authkey if user has editGrant to displaycontainer
994                 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]) {
995                     continue;
996                 }
997                 
998                 // keep authkey if attender is a contact (no account) and user has editGrant for event
999                 if ($attender->user_type == self::USERTYPE_USER
1000                     && $attender->user_id instanceof Tinebase_Record_Abstract
1001                     && (!$attender->user_id->has('account_id') || !$attender->user_id->account_id)
1002                     && (!$event || $event->{Tinebase_Model_Grants::GRANT_EDIT})
1003                 ) {
1004                     continue;
1005                 }
1006                 
1007                 $attender->status_authkey = NULL;
1008             }
1009         }
1010     }
1011     
1012     /**
1013      * checks if given alarm should be send to given attendee
1014      * 
1015      * @param  Calendar_Model_Attender $_attendee
1016      * @param  Tinebase_Model_Alarm    $_alarm
1017      * @return bool
1018      */
1019     public static function isAlarmForAttendee($_attendee, $_alarm, $_event=NULL)
1020     {
1021         // attendee: array with one user_type/id if alarm is for one attendee only
1022         $attendeeOption = $_alarm->getOption('attendee');
1023         
1024         // skip: array of array of user_type/id with attendees this alarm is to skip for
1025         $skipOption = $_alarm->getOption('skip');
1026         
1027         if ($attendeeOption) {
1028             return (bool) self::getAttendee(new Tinebase_Record_RecordSet('Calendar_Model_Attender', array($_attendee)), new Calendar_Model_Attender($attendeeOption));
1029         }
1030         
1031         if (is_array($skipOption)) {
1032             $skipAttendees = new Tinebase_Record_RecordSet('Calendar_Model_Attender', $skipOption);
1033             if(self::getAttendee($skipAttendees, $_attendee)) {
1034                 return false;
1035             }
1036         }
1037         
1038         $isOrganizerCondition = $_event ? $_event->isOrganizer($_attendee) : TRUE;
1039         $isAttendeeCondition = $_event && $_event->attendee instanceof Tinebase_Record_RecordSet ? self::getAttendee($_event->attendee, $_attendee) : TRUE;
1040         return ($isAttendeeCondition || $isOrganizerCondition)&& $_attendee->status != Calendar_Model_Attender::STATUS_DECLINED;
1041     }
1042
1043     /**
1044      * clear in class cache
1045      */
1046     public static function clearCache()
1047     {
1048         foreach(self::$_resolvedAttendeesCache as $name => $entries) {
1049             self::$_resolvedAttendeesCache[$name] = array();
1050         }
1051     }
1052 }