0011522: improve handling of group-lists
[tine20] / tine20 / Addressbook / Controller / List.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Addressbook
6  * @subpackage  Controller
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  * @copyright   Copyright (c) 2010-2012 Metaways Infosystems GmbH (http://www.metaways.de)
10  * 
11  */
12
13 /**
14  * contact controller for Addressbook
15  *
16  * @package     Addressbook
17  * @subpackage  Controller
18  */
19 class Addressbook_Controller_List extends Tinebase_Controller_Record_Abstract
20 {
21     /**
22      * application name (is needed in checkRight())
23      *
24      * @var string
25      */
26     protected $_applicationName = 'Addressbook';
27
28     /**
29      * Model name
30      *
31      * @var string
32      */
33     protected $_modelName = 'Addressbook_Model_List';
34
35     /**
36      * @var null|Tinebase_Backend_Sql
37      */
38     protected $_memberRolesBackend = null;
39
40     /**
41      * the constructor
42      *
43      * don't use the constructor. use the singleton
44      */
45     private function __construct()
46     {
47         $this->_resolveCustomFields = true;
48         $this->_backend = new Addressbook_Backend_List();
49     }
50
51     /**
52      * don't clone. Use the singleton.
53      *
54      */
55     private function __clone()
56     {
57     }
58
59     /**
60      * holds the instance of the singleton
61      *
62      * @var Addressbook_Controller_List
63      */
64     private static $_instance = NULL;
65
66     protected function _getMemberRolesBackend()
67     {
68         if ($this->_memberRolesBackend === null) {
69             $this->_memberRolesBackend = new Tinebase_Backend_Sql(array(
70                 'tableName' => 'adb_list_m_role',
71                 'modelName' => 'Addressbook_Model_ListMemberRole',
72             ));
73         }
74
75         return $this->_memberRolesBackend;
76     }
77
78     /**
79      * the singleton pattern
80      *
81      * @return Addressbook_Controller_List
82      */
83     public static function getInstance()
84     {
85         if (self::$_instance === NULL) {
86             self::$_instance = new Addressbook_Controller_List();
87         }
88
89         return self::$_instance;
90     }
91
92     /**
93      * (non-PHPdoc)
94      *
95      * @see Tinebase_Controller_Record_Abstract::get()
96      */
97     public function get($_id, $_containerId = NULL)
98     {
99         $result = new Tinebase_Record_RecordSet('Addressbook_Model_List', array(parent::get($_id, $_containerId)));
100         $this->_removeHiddenListMembers($result);
101         return $result->getFirstRecord();
102     }
103
104     /**
105      * use contact search to remove hidden list members
106      *
107      * @param Tinebase_Record_RecordSet $lists
108      */
109     protected function _removeHiddenListMembers($lists)
110     {
111         if (count($lists) === 0) {
112             return;
113         }
114
115         $allMemberIds = array();
116         foreach ($lists as $list) {
117             $allMemberIds = array_merge($list->members, $allMemberIds);
118         }
119         $allMemberIds = array_unique($allMemberIds);
120
121         if (empty($allMemberIds)) {
122             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
123                 . ' No members found.');
124             return;
125         }
126
127         $allVisibleMemberIds = Addressbook_Controller_Contact::getInstance()->search(new Addressbook_Model_ContactFilter(array(array(
128             'field' => 'id',
129             'operator' => 'in',
130             'value' => $allMemberIds
131         ))), NULL, FALSE, TRUE);
132
133         $hiddenMemberids = array_diff($allMemberIds, $allVisibleMemberIds);
134
135         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
136             . ' Found ' . count($hiddenMemberids) . ' hidden members, removing them');
137         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
138             . print_r($hiddenMemberids, TRUE));
139
140         foreach ($lists as $list) {
141             $list->members = array_diff($list->members, $hiddenMemberids);
142         }
143     }
144
145     /**
146      * (non-PHPdoc)
147      *
148      * @see Tinebase_Controller_Record_Abstract::search()
149      */
150     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
151     {
152         $result = parent::search($_filter, $_pagination, $_getRelations, $_onlyIds, $_action);
153
154         if ($_onlyIds !== true) {
155             $this->_removeHiddenListMembers($result);
156         }
157
158         return $result;
159     }
160
161     /**
162      * (non-PHPdoc)
163      *
164      * @see Tinebase_Controller_Record_Abstract::getMultiple()
165      */
166     public function getMultiple($_ids, $_ignoreACL = FALSE)
167     {
168         $result = parent::getMultiple($_ids, $_ignoreACL);
169         $this->_removeHiddenListMembers($result);
170         return $result;
171     }
172
173     /**
174      * add new members to list
175      *
176      * @param  mixed $_listId
177      * @param  mixed $_newMembers
178      * @param  boolean $_addToGroup
179      * @return Addressbook_Model_List
180      */
181     public function addListMember($_listId, $_newMembers, $_addToGroup = true)
182     {
183         try {
184             $list = $this->get($_listId);
185         } catch (Tinebase_Exception_AccessDenied $tead) {
186             $this->_fixEmptyContainerId($_listId);
187             $list = $this->get($_listId);
188         }
189
190         $this->_checkGrant($list, 'update', TRUE, 'No permission to add list member.');
191         $this->_checkGroupGrant($list, TRUE, 'No permission to add list member.');
192
193         $list = $this->_backend->addListMember($_listId, $_newMembers);
194
195         if (true === $_addToGroup && ! empty($list->group_id)) {
196             foreach (Tinebase_Record_RecordSet::getIdsFromMixed($_newMembers) as $userId) {
197                 Admin_Controller_Group::getInstance()->addGroupMember($list->group_id, $userId, false);
198             }
199         }
200
201         return $this->get($list->getId());
202     }
203
204     protected function _checkGroupGrant($_list, $_throw = false, $_msg = '')
205     {
206         if (! empty($_list->group_id)) {
207             if (!Tinebase_Core::getUser()->hasRight('Admin', Admin_Acl_Rights::MANAGE_ACCOUNTS)) {
208                 if ($_throw) {
209                     throw new Tinebase_Exception_AccessDenied($_msg);
210                 } else {
211                     return false;
212                 }
213             }
214         }
215         return true;
216     }
217
218     /**
219      * fixes empty container ids / perhaps this can be removed later as all lists should have a container id!
220      *
221      * @param  mixed $_listId
222      * @return Addressbook_Model_List
223      */
224     protected function _fixEmptyContainerId($_listId)
225     {
226         $list = $this->_backend->get($_listId);
227
228         if (empty($list->container_id)) {
229             $list->container_id = $this->_getDefaultInternalAddressbook();
230             $list = $this->_backend->update($list);
231         }
232
233         return $list;
234     }
235
236     /**
237      * get default internal adb id
238      *
239      * @return string
240      */
241     protected function _getDefaultInternalAddressbook()
242     {
243         $appConfigDefaults = Admin_Controller::getInstance()->getConfigSettings();
244         $result = (isset($appConfigDefaults[Admin_Model_Config::DEFAULTINTERNALADDRESSBOOK])) ? $appConfigDefaults[Admin_Model_Config::DEFAULTINTERNALADDRESSBOOK] : NULL;
245
246         if (empty($result)) {
247             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
248                 . ' Default internal addressbook not found. Creating new config setting.');
249             $result = Addressbook_Setup_Initialize::setDefaultInternalAddressbook()->getId();
250         }
251         return $result;
252     }
253
254     /**
255      * remove members from list
256      *
257      * @param  mixed $_listId
258      * @param  mixed $_removeMembers
259      * @param  boolean $_removeFromGroup
260      * @return Addressbook_Model_List
261      */
262     public function removeListMember($_listId, $_removeMembers, $_removeFromGroup = true)
263     {
264         $list = $this->get($_listId);
265
266         $this->_checkGrant($list, 'update', TRUE, 'No permission to remove list member.');
267         $this->_checkGroupGrant($list, TRUE, 'No permission to remove list member.');
268
269         $list = $this->_backend->removeListMember($_listId, $_removeMembers);
270
271         if (true === $_removeFromGroup && ! empty($list->group_id)) {
272             foreach (Tinebase_Record_RecordSet::getIdsFromMixed($_removeMembers) as $userId) {
273                 Admin_Controller_Group::getInstance()->removeGroupMember($list->group_id, $userId, false);
274             }
275         }
276
277         return $this->get($list->getId());
278     }
279
280     /**
281      * inspect creation of one record
282      *
283      * @param   Tinebase_Record_Interface $_record
284      * @return  void
285      */
286     protected function _inspectBeforeCreate(Tinebase_Record_Interface $_record)
287     {
288         if (isset($_record->type) && $_record->type == Addressbook_Model_List::LISTTYPE_GROUP) {
289             if (empty($_record->group_id)) {
290                 throw Tinebase_Exception_UnexpectedValue('group_id is empty, must not happen for list type group');
291             }
292
293             // check rights
294             $this->_checkGroupGrant($_record, TRUE, 'can not add list of type ' . Addressbook_Model_List::LISTTYPE_GROUP);
295
296             // check if group is there, if not => not found exception
297             Admin_Controller_Group::getInstance()->get($_record->group_id);
298         }
299     }
300
301     /**
302      * inspect creation of one record (after create)
303      *
304      * @param   Tinebase_Record_Interface $_createdRecord
305      * @param   Tinebase_Record_Interface $_record
306      * @return  void
307      */
308     protected function _inspectAfterCreate($_createdRecord, Tinebase_Record_Interface $_record)
309     {
310         $this->_fireChangeListeEvent($_createdRecord);
311     }
312
313     /**
314      * inspect update of one record
315      *
316      * @param   Tinebase_Record_Interface $_record the update record
317      * @param   Tinebase_Record_Interface $_oldRecord the current persistent record
318      * @return  void
319      */
320     protected function _inspectBeforeUpdate($_record, $_oldRecord)
321     {
322         if (! empty($_record->group_id)) {
323
324             // first check if something changed that requires special rights
325             $changeGroup = false;
326             foreach (Addressbook_Model_List::getManageAccountFields() as $field) {
327                 if ($_record->{$field} != $_oldRecord->{$field}) {
328                     $changeGroup = true;
329                     break;
330                 }
331             }
332
333             // then do the update, the group controller will check manage accounts right
334             if ($changeGroup) {
335                 $groupController = Admin_Controller_Group::getInstance();
336                 $group = $groupController->get($_record->group_id);
337
338                 foreach (Addressbook_Model_List::getManageAccountFields() as $field) {
339                     $group->{$field} = $_record->{$field};
340                 }
341
342                 $groupController->update($group, false);
343             }
344         }
345     }
346
347     /**
348      * inspect update of one record (after update)
349      *
350      * @param   Tinebase_Record_Interface $updatedRecord   the just updated record
351      * @param   Tinebase_Record_Interface $record          the update record
352      * @param   Tinebase_Record_Interface $currentRecord   the current record (before update)
353      * @return  void
354      */
355     protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord)
356     {
357         $this->_fireChangeListeEvent($updatedRecord);
358     }
359
360     /**
361      * fireChangeListeEvent
362      *
363      * @param Addressbook_Model_List $list
364      */
365     protected function _fireChangeListeEvent(Addressbook_Model_List $list)
366     {
367         $event = new Addressbook_Event_ChangeList();
368         $event->list = $list;
369         Tinebase_Event::fireEvent($event);
370     }
371
372     /**
373      * inspects delete action
374      *
375      * @param array $_ids
376      * @return array of ids to actually delete
377      */
378     protected function _inspectDelete(array $_ids)
379     {
380         $lists = $this->getMultiple($_ids);
381         foreach ($lists as $list) {
382             $event = new Addressbook_Event_DeleteList();
383             $event->list = $list;
384             Tinebase_Event::fireEvent($event);
385         }
386
387         return $_ids;
388     }
389
390     /**
391      * create or update list in addressbook sql backend
392      *
393      * @param  Tinebase_Model_Group $group
394      * @return Addressbook_Model_List
395      */
396     public function createOrUpdateByGroup(Tinebase_Model_Group $group)
397     {
398         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($group->toArray(), TRUE));
399
400         try {
401             if (empty($group->list_id)) {
402                 $list = $this->_backend->getByGroupName($group->name);
403                 if (!$list) {
404                     // jump to catch block => no list_id provided and no existing list for group found
405                     throw new Tinebase_Exception_NotFound('list_id is empty');
406                 }
407                 $group->list_id = $list->getId();
408             } else {
409                 $list = $this->_backend->get($group->list_id);
410             }
411
412             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
413                 . ' Update list ' . $group->name);
414
415             $list->name = $group->name;
416             $list->description = $group->description;
417             $list->email = $group->email;
418             $list->type = Addressbook_Model_List::LISTTYPE_GROUP;
419             $list->container_id = (empty($group->container_id)) ? $this->_getDefaultInternalAddressbook() : $group->container_id;
420             $list->members = (isset($group->members)) ? $this->_getContactIds($group->members) : array();
421
422             // add modlog info
423             Tinebase_Timemachine_ModificationLog::setRecordMetaData($list, 'update');
424
425             $list = $this->_backend->update($list);
426             $list = $this->get($list->getId());
427
428         } catch (Tinebase_Exception_NotFound $tenf) {
429             $list = $this->createByGroup($group);
430         }
431
432         return $list;
433     }
434
435     /**
436      * create new list by group
437      *
438      * @param Tinebase_Model_Group $group
439      * @return Addressbook_Model_List
440      */
441     public function createByGroup($group)
442     {
443         $list = new Addressbook_Model_List(array(
444             'name' => $group->name,
445             'description' => $group->description,
446             'email' => $group->email,
447             'type' => Addressbook_Model_List::LISTTYPE_GROUP,
448             'container_id' => (empty($group->container_id)) ? $this->_getDefaultInternalAddressbook() : $group->container_id,
449             'members' => (isset($group->members)) ? $this->_getContactIds($group->members) : array(),
450         ));
451
452         // add modlog info
453         Tinebase_Timemachine_ModificationLog::setRecordMetaData($list, 'create');
454
455         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
456             . ' Add new list ' . $group->name);
457         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
458             . ' ' . print_r($list->toArray(), TRUE));
459
460         $list = $this->_backend->create($list);
461
462         return $list;
463     }
464
465     /**
466      * get contact_ids of users
467      *
468      * @param  array $_userIds
469      * @return array
470      */
471     protected function _getContactIds($_userIds)
472     {
473         $contactIds = array();
474
475         if (empty($_userIds)) {
476             return $contactIds;
477         }
478
479         foreach ($_userIds as $userId) {
480             $user = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $userId);
481             if (!empty($user->contact_id)) {
482                 $contactIds[] = $user->contact_id;
483             }
484         }
485
486         return $contactIds;
487     }
488
489     /**
490      * you can define default filters here
491      *
492      * @param Tinebase_Model_Filter_FilterGroup $_filter
493      */
494     protected function _addDefaultFilter(Tinebase_Model_Filter_FilterGroup $_filter = NULL)
495     {
496         if (!$_filter->isFilterSet('showHidden')) {
497             $hiddenFilter = $_filter->createFilter('showHidden', 'equals', FALSE);
498             $hiddenFilter->setIsImplicit(TRUE);
499             $_filter->addFilter($hiddenFilter);
500         }
501     }
502
503     /**
504      * set relations / tags / alarms
505      *
506      * @param   Tinebase_Record_Interface $updatedRecord the just updated record
507      * @param   Tinebase_Record_Interface $record the update record
508      * @param   Tinebase_Record_Interface $currentRecord   the original record if one exists
509      * @param   boolean                   $returnUpdatedRelatedData
510      * @return  Tinebase_Record_Interface
511      */
512     protected function _setRelatedData(Tinebase_Record_Interface $updatedRecord, Tinebase_Record_Interface $record, Tinebase_Record_Interface $currentRecord = null, $returnUpdatedRelatedData = FALSE)
513     {
514         if (isset($record->memberroles)) {
515             // get migration
516             // TODO add generic helper fn for this?
517             $memberrolesToSet = (!$record->memberroles instanceof Tinebase_Record_RecordSet)
518                 ? new Tinebase_Record_RecordSet(
519                     'Addressbook_Model_ListMemberRole',
520                     $record->memberroles,
521                     /* $_bypassFilters */ true
522                 ) : $record->memberroles;
523
524             foreach ($memberrolesToSet as $memberrole) {
525                 foreach (array('contact_id', 'list_role_id', 'list_id') as $field) {
526                     if (isset($memberrole[$field]['id'])) {
527                         $memberrole[$field] = $memberrole[$field]['id'];
528                     }
529                 }
530             }
531
532             $currentMemberroles = $this->_getMemberRoles($record);
533             $diff = $currentMemberroles->diff($memberrolesToSet);
534             if (count($diff['added']) > 0) {
535                 $diff['added']->list_id = $updatedRecord->getId();
536                 foreach ($diff['added'] as $memberrole) {
537                     $this->_getMemberRolesBackend()->create($memberrole);
538                 }
539             }
540             if (count($diff['removed']) > 0) {
541                 $this->_getMemberRolesBackend()->delete($diff['removed']->getArrayOfIds());
542             }
543         }
544
545         $result = parent::_setRelatedData($updatedRecord, $record, $currentRecord, $returnUpdatedRelatedData);
546
547         return $result;
548     }
549
550     /**
551      * add related data to record
552      *
553      * @param Tinebase_Record_Interface $record
554      */
555     protected function _getRelatedData($record)
556     {
557         $memberRoles = $this->_getMemberRoles($record);
558         if (count($memberRoles) > 0) {
559             $record->memberroles = $memberRoles;
560         }
561         parent::_getRelatedData($record);
562     }
563
564     protected function _getMemberRoles($record)
565     {
566         $result = $this->_getMemberRolesBackend()->getMultipleByProperty($record->getId(), 'list_id');
567         return $result;
568     }
569
570     /**
571      * get all lists given contact is member of
572      *
573      * @param $contact
574      * @return array
575      */
576     public function getMemberships($contact)
577     {
578         return $this->_backend->getMemberships($contact);
579     }
580 }