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