0011172: optimize getGroupmemberships in Principalbackend
[tine20] / tine20 / Tinebase / WebDav / PrincipalBackend.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  WebDav
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2011-2014 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  */
11
12 /**
13  * principal backend class
14  * 
15  * @package     Tinebase
16  * @subpackage  WebDav
17  */
18 class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend\BackendInterface
19 {
20     const PREFIX_USERS  = 'principals/users';
21     const PREFIX_GROUPS = 'principals/groups';
22     const SHARED        = 'shared';
23     
24     /**
25      * (non-PHPdoc)
26      * @see Sabre\DAVACL\IPrincipalBackend::getPrincipalsByPrefix()
27      */
28     public function getPrincipalsByPrefix($prefixPath) 
29     {
30         $principals = array();
31         
32         switch ($prefixPath) {
33             case self::PREFIX_GROUPS:
34                 $filter = new Addressbook_Model_ListFilter(array(
35                     array(
36                         'field'     => 'type',
37                         'operator'  => 'equals',
38                         'value'     => Addressbook_Model_List::LISTTYPE_GROUP
39                     )
40                 ));
41                 
42                 $lists = Addressbook_Controller_List::getInstance()->search($filter);
43                 
44                 foreach ($lists as $list) {
45                     $principals[] = $this->_listToPrincipal($list);
46                 }
47                 
48                 break;
49                 
50             case self::PREFIX_USERS:
51                 $filter = $this->_getContactFilterForUserContact();
52                 
53                 $contacts = Addressbook_Controller_Contact::getInstance()->search($filter);
54                 
55                 foreach ($contacts as $contact) {
56                     $principals[] = $this->_contactToPrincipal($contact);
57                 }
58                 
59                 $principals[] = $this->_contactForSharedPrincipal();
60                 
61                 break;
62         }
63         
64         return $principals;
65     }
66     
67     /**
68      * (non-PHPdoc)
69      * @see Sabre\DAVACL\IPrincipalBackend::getPrincipalByPath()
70      * @todo resolve real $path
71      */
72     public function getPrincipalByPath($path) 
73     {
74         // any user has to lookup the data at least once
75         $cacheId = Tinebase_Helper::convertCacheId('getPrincipalByPath' . Tinebase_Core::getUser()->getId() . $path);
76         
77         $principal = Tinebase_Core::getCache()->load($cacheId);
78         if ($principal !== false) {
79             return $principal;
80         }
81         
82         $principal = null;
83         
84         list($prefix, $id) = \Sabre\DAV\URLUtil::splitPath($path);
85         
86         // special handling for calendar proxy principals
87         // they are groups in the user namespace
88         if (in_array($id, array('calendar-proxy-read', 'calendar-proxy-write'))) {
89             $path = $prefix;
90             
91             // set prefix to calendar-proxy-read or calendar-proxy-write
92             $prefix = $id;
93             
94             list(, $id) = \Sabre\DAV\URLUtil::splitPath($path);
95         }
96         
97         switch ($prefix) {
98             case 'calendar-proxy-read':
99                 return null;
100                 
101                 break;
102                 
103             case 'calendar-proxy-write':
104                 // does the account exist
105                 $contactPrincipal = $this->getPrincipalByPath(self::PREFIX_USERS . '/' . $id);
106                 
107                 if (! $contactPrincipal) {
108                     if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(
109                             __METHOD__ . '::' . __LINE__ . ' Account principal does not exist: ' . $id);
110                     return null;
111                 }
112                 
113                 $principal = array(
114                     'uri'                     => $contactPrincipal['uri'] . '/' . $prefix,
115                     '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-user-type'  => 'GROUP',
116                     '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}record-type' => 'groups'
117                 );
118                 
119                 break;
120                 
121             case self::PREFIX_GROUPS:
122                 $filter = new Addressbook_Model_ListFilter(array(
123                     array(
124                         'field'     => 'type',
125                         'operator'  => 'equals',
126                         'value'     => Addressbook_Model_List::LISTTYPE_GROUP
127                     ),
128                     array(
129                         'field'     => 'id',
130                         'operator'  => 'equals',
131                         'value'     => $id
132                     ),
133                 ));
134                 
135                 $list = Addressbook_Controller_List::getInstance()->search($filter)->getFirstRecord();
136                 
137                 if (! $list) {
138                     if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(
139                             __METHOD__ . '::' . __LINE__ . ' Group/list principal does not exist: ' . $id);
140                     return null;
141                 }
142                 
143                 $principal = $this->_listToPrincipal($list);
144                 
145                 break;
146                 
147             case self::PREFIX_USERS:
148                 if ($id === self::SHARED) {
149                     $principal = $this->_contactForSharedPrincipal();
150                     
151                 } else {
152                     $filter = $this->_getContactFilterForUserContact($id);
153                     
154                     $contact = Addressbook_Controller_Contact::getInstance()->search($filter)->getFirstRecord();
155                     
156                     if (! $contact) {
157                         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(
158                             __METHOD__ . '::' . __LINE__ . ' Contact principal does not exist: ' . $id);
159                         return null;
160                     }
161                     
162                     $principal = $this->_contactToPrincipal($contact);
163                 }
164                 
165                 break;
166         }
167         
168         Tinebase_Core::getCache()->save($principal, $cacheId, array(), /* 1 minute */ 60);
169         
170         return $principal;
171     }
172     
173     /**
174      * get contact filter
175      * 
176      * @param string $id
177      * @return Addressbook_Model_ContactFilter
178      */
179     protected function _getContactFilterForUserContact($id = null)
180     {
181         $filterData = array(array(
182             'field'     => 'type',
183             'operator'  => 'equals',
184             'value'     => Addressbook_Model_Contact::CONTACTTYPE_USER
185         ));
186         
187         if ($id !== null) {
188             $filterData[] = array(
189                 'field'     => 'id',
190                 'operator'  => 'equals',
191                 'value'     => $id
192             );
193         }
194         
195         return new Addressbook_Model_ContactFilter($filterData);
196     }
197     
198     /**
199      * (non-PHPdoc)
200      * @see Sabre\DAVACL\IPrincipalBackend::getGroupMemberSet()
201      */
202     public function getGroupMemberSet($principal) 
203     {
204         $result = array();
205         
206         list($prefix, $id) = \Sabre\DAV\URLUtil::splitPath($principal);
207         
208         // special handling for calendar proxy principals
209         // they are groups in the user namespace
210         if (in_array($id, array('calendar-proxy-read', 'calendar-proxy-write'))) {
211             $path = $prefix;
212             
213             // set prefix to calendar-proxy-read or calendar-proxy-write
214             $prefix = $id;
215             
216             list(, $id) = \Sabre\DAV\URLUtil::splitPath($path);
217         }
218         
219         switch ($prefix) {
220             case 'calendar-proxy-read':
221                 return array();
222                 
223                 break;
224                 
225             case 'calendar-proxy-write':
226                 $result = array();
227                 
228                 $applications = array(
229                     'Calendar' => 'Calendar_Model_Event',
230                     'Tasks'    => 'Tasks_Model_Task'
231                 );
232                 
233                 foreach ($applications as $application => $model) {
234                     if ($id === self::SHARED) {
235                         // check if account has the right to run the calendar at all
236                         if (!Tinebase_Acl_Roles::getInstance()->hasRight($application, Tinebase_Core::getUser(), Tinebase_Acl_Rights::RUN)) {
237                             continue;
238                         }
239                         
240                         // collect all users which have access to any of the calendars of this user
241                         $sharedContainerSync = Tinebase_Container::getInstance()->getSharedContainer(Tinebase_Core::getUser(), $model, Tinebase_Model_Grants::GRANT_SYNC);
242                         
243                         if ($sharedContainerSync->count() > 0) {
244                             $sharedContainerRead = Tinebase_Container::getInstance()->getSharedContainer(Tinebase_Core::getUser(), $model, Tinebase_Model_Grants::GRANT_READ);
245                             
246                             $sharedContainerIds = array_intersect($sharedContainerSync->getArrayOfIds(), $sharedContainerRead->getArrayOfIds());
247                             
248                             $result = array_merge(
249                                 $result,
250                                 $this->_containerGrantsToPrincipals($sharedContainerSync->filter('id', $sharedContainerIds)));
251                         }
252                     } else {
253                         $filter = $this->_getContactFilterForUserContact($id);
254                         
255                         $contact = Addressbook_Controller_Contact::getInstance()->search($filter)->getFirstRecord();
256                         
257                         if (!$contact instanceof Addressbook_Model_Contact || !$contact->account_id) {
258                             continue;
259                         }
260                         
261                         // check if account has the right to run the calendar at all
262                         if (!Tinebase_Acl_Roles::getInstance()->hasRight($application, $contact->account_id, Tinebase_Acl_Rights::RUN)) {
263                             continue;
264                         }
265                         
266                         // collect all users which have access to any of the calendars of this user
267                         $personalContainerSync = Tinebase_Container::getInstance()->getPersonalContainer(Tinebase_Core::getUser(), $model, $contact->account_id, Tinebase_Model_Grants::GRANT_SYNC);
268                         
269                         if ($personalContainerSync->count() > 0) {
270                             $personalContainerRead = Tinebase_Container::getInstance()->getPersonalContainer(Tinebase_Core::getUser(), $model, $contact->account_id, Tinebase_Model_Grants::GRANT_READ);
271                             
272                             $personalContainerIds = array_intersect($personalContainerSync->getArrayOfIds(), $personalContainerRead->getArrayOfIds());
273                             
274                             $result = array_merge(
275                                 $result,
276                                 $this->_containerGrantsToPrincipals($personalContainerSync->filter('id', $personalContainerIds))
277                             );
278                         }
279                     }
280                 }
281                 
282                 break;
283                 
284             case self::PREFIX_GROUPS:
285                 $filter = new Addressbook_Model_ListFilter(array(
286                     array(
287                         'field'     => 'type',
288                         'operator'  => 'equals',
289                         'value'     => Addressbook_Model_List::LISTTYPE_GROUP
290                     ),
291                     array(
292                         'field'     => 'id',
293                         'operator'  => 'equals',
294                         'value'     => $id
295                     ),
296                 ));
297                 
298                 $list = Addressbook_Controller_List::getInstance()->search($filter)->getFirstRecord();
299                 
300                 if (!$list) {
301                     return array();
302                 }
303                 
304                 foreach ($list->members as $member) {
305                     $result[] = self::PREFIX_USERS . '/' . $member;
306                 }
307                 
308                 break;
309         }
310         
311         return $result;
312     }
313     
314     /**
315      * (non-PHPdoc)
316      * @see Sabre\DAVACL\IPrincipalBackend::getGroupMembership()
317      */
318     public function getGroupMembership($principal)
319     {
320         $result = array();
321         
322         list($prefix, $contactId) = \Sabre\DAV\URLUtil::splitPath($principal);
323         
324         switch ($prefix) {
325             case self::PREFIX_GROUPS:
326                 // @TODO implement?
327                 break;
328         
329             case self::PREFIX_USERS:
330                 if ($contactId !== self::SHARED) {
331                     $classCacheId = $principal;
332                     
333                     try {
334                         return Tinebase_Cache_PerRequest::getInstance()->load(__CLASS__, __FUNCTION__, $classCacheId);
335                     } catch (Tinebase_Exception_NotFound $tenf) {
336                         // continue...
337                     }
338                     
339                     $cacheId = __FUNCTION__ . sha1($classCacheId);
340                     
341                     // try to load from cache
342                     $cache  = Tinebase_Core::getCache();
343                     $result = $cache->load($cacheId);
344                     
345                     if ($result !== FALSE) {
346                         Tinebase_Cache_PerRequest::getInstance()->save(__CLASS__, __FUNCTION__, $classCacheId, $result);
347                         
348                         return $result;
349                     }
350                     $result = array();
351                     
352                     $user = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('contactId', $contactId);
353                     
354                     $groupIds = Tinebase_Group::getInstance()->getGroupMemberships($user);
355                     $groups   = Tinebase_Group::getInstance()->getMultiple($groupIds);
356                     
357                     foreach ($groups as $group) {
358                         if ($group->list_id && $group->visibility == Tinebase_Model_Group::VISIBILITY_DISPLAYED) {
359                             $result[] = self::PREFIX_GROUPS . '/' . $group->list_id;
360                         }
361                     }
362                     
363                     if (Tinebase_Core::getUser()->hasRight('Calendar', Tinebase_Acl_Rights::RUN)) {
364                         // return users only, if they have the sync AND read grant set
365                         $otherUsers = Tinebase_Container::getInstance()->getOtherUsers($user, 'Calendar', array(Tinebase_Model_Grants::GRANT_SYNC, Tinebase_Model_Grants::GRANT_READ), false, true);
366                         foreach ($otherUsers as $u) {
367                             if ($u->contact_id && $u->visibility == Tinebase_Model_User::VISIBILITY_DISPLAYED) {
368                                 $result[] = self::PREFIX_USERS . '/' . $u->contact_id . '/calendar-proxy-write';
369                             }
370                         }
371                         
372                         // return containers only, if the user has the sync AND read grant set
373                         $sharedContainers = Tinebase_Container::getInstance()->getSharedContainer($user, 'Calendar', array(Tinebase_Model_Grants::GRANT_SYNC, Tinebase_Model_Grants::GRANT_READ), false, true);
374                         
375                         if ($sharedContainers->count() > 0) {
376                             $result[] = self::PREFIX_USERS . '/' . self::SHARED . '/calendar-proxy-write';
377                         }
378                     }
379                     Tinebase_Cache_PerRequest::getInstance()->save(__CLASS__, __FUNCTION__, $classCacheId, $result);
380                     $cache->save($result, $cacheId, array(), 60 * 3);
381                 }
382                 
383                 break;
384         }
385         
386         return $result;
387     }
388     
389     public function setGroupMemberSet($principal, array $members) 
390     {
391         // do nothing
392     }
393     
394     public function updatePrincipal($path, $mutations)
395     {
396         return false;
397     }
398     
399     /**
400      * This method is used to search for principals matching a set of
401      * properties.
402      *
403      * This search is specifically used by RFC3744's principal-property-search
404      * REPORT. You should at least allow searching on
405      * http://sabredav.org/ns}email-address.
406      *
407      * The actual search should be a unicode-non-case-sensitive search. The
408      * keys in searchProperties are the WebDAV property names, while the values
409      * are the property values to search on.
410      *
411      * If multiple properties are being searched on, the search should be
412      * AND'ed.
413      *
414      * This method should simply return an array with full principal uri's.
415      *
416      * If somebody attempted to search on a property the backend does not
417      * support, you should simply return 0 results.
418      *
419      * You can also just return 0 results if you choose to not support
420      * searching at all, but keep in mind that this may stop certain features
421      * from working.
422      *
423      * @param string $prefixPath
424      * @param array $searchProperties
425      * @todo implement handling for shared pseudo user
426      * @return array
427      */
428     public function searchPrincipals($prefixPath, array $searchProperties)
429     {
430         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
431             __METHOD__ . '::' . __LINE__ . ' path: ' . $prefixPath . ' properties: ' . print_r($searchProperties, true));
432         
433         $principalUris = array();
434         
435         switch ($prefixPath) {
436             case self::PREFIX_GROUPS:
437                 $filter = new Addressbook_Model_ListFilter(array(
438                     array(
439                         'field'     => 'type',
440                         'operator'  => 'equals',
441                         'value'     => Addressbook_Model_List::LISTTYPE_GROUP
442                     )
443                 ));
444                 
445                 if (!empty($searchProperties['{http://calendarserver.org/ns/}search-token'])) {
446                     $filter->addFilter($filter->createFilter(array(
447                         'field'     => 'query',
448                         'operator'  => 'contains',
449                         'value'     => $searchProperties['{http://calendarserver.org/ns/}search-token']
450                     )));
451                 }
452                 
453                 if (!empty($searchProperties['{DAV:}displayname'])) {
454                     $filter->addFilter($filter->createFilter(array(
455                         'field'     => 'name',
456                         'operator'  => 'contains',
457                         'value'     => $searchProperties['{DAV:}displayname']
458                     )));
459                 }
460                 
461                 $result = Addressbook_Controller_List::getInstance()->search($filter, null, false, true);
462                 
463                 foreach ($result as $listId) {
464                     $principalUris[] = $prefixPath . '/' . $listId;
465                 }
466                 
467                 break;
468                 
469             case self::PREFIX_USERS:
470                 $filter = $this->_getContactFilterForUserContact();
471                 
472                 if (!empty($searchProperties['{http://calendarserver.org/ns/}search-token'])) {
473                     $filter->addFilter($filter->createFilter(array(
474                         'field'     => 'query',
475                         'operator'  => 'contains',
476                         'value'     => $searchProperties['{http://calendarserver.org/ns/}search-token']
477                     )));
478                 }
479                 
480                 if (!empty($searchProperties['{http://sabredav.org/ns}email-address'])) {
481                     $filter->addFilter($filter->createFilter(array(
482                         'field'     => 'email_query',
483                         'operator'  => 'contains',
484                         'value'     => $searchProperties['{http://sabredav.org/ns}email-address']
485                     )));
486                 }
487                 
488                 if (!empty($searchProperties['{DAV:}displayname'])) {
489                     $filter->addFilter($filter->createFilter(array(
490                         'field'     => 'query',
491                         'operator'  => 'contains',
492                         'value'     => $searchProperties['{DAV:}displayname']
493                     )));
494                 }
495                 
496                 if (!empty($searchProperties['{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}first-name'])) {
497                     $filter->addFilter($filter->createFilter(array(
498                         'field'     => 'n_given',
499                         'operator'  => 'contains',
500                         'value'     => $searchProperties['{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}first-name']
501                     )));
502                 }
503                 
504                 if (!empty($searchProperties['{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}last-name'])) {
505                     $filter->addFilter($filter->createFilter(array(
506                         'field'     => 'n_family',
507                         'operator'  => 'contains',
508                         'value'     => $searchProperties['{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}last-name']
509                     )));
510                 }
511                 
512                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
513                     ' path: ' . $prefixPath . ' properties: ' . print_r($filter->toArray(), true));
514                 
515                 $result = Addressbook_Controller_Contact::getInstance()->search($filter, null, false, true);
516                 
517                 foreach ($result as $contactId) {
518                     $principalUris[] = $prefixPath . '/' . $contactId;
519                 }
520                 
521                 break;
522         }
523         
524         return $principalUris;
525     }
526     
527     /**
528      * return shared pseudo principal (principal for the shared containers) 
529      */
530     protected function _contactForSharedPrincipal()
531     {
532         $translate = Tinebase_Translation::getTranslation('Tinebase');
533         
534         $principal = array(
535             'uri'                     => self::PREFIX_USERS . '/' . self::SHARED,
536             '{DAV:}displayname'       => $translate->_('Shared folders'),
537             
538             '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-user-type'  => 'INDIVIDUAL',
539             
540             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}record-type' => 'users',
541             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}first-name'  => 'Folders',
542             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}last-name'   => 'Shared'
543         );
544         
545         return $principal;
546         
547     }
548     
549     /**
550      * convert contact model to principal array
551      * 
552      * @param Addressbook_Model_Contact $contact
553      * @return array
554      */
555     protected function _contactToPrincipal(Addressbook_Model_Contact $contact)
556     {
557         $principal = array(
558             'uri'                     => self::PREFIX_USERS . '/' . $contact->getId(),
559             '{DAV:}displayname'       => $contact->n_fileas,
560             '{DAV:}alternate-URI-set' => array('urn:uuid:' . $contact->getId()),
561             
562             '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-user-type'  => 'INDIVIDUAL',
563             
564             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}record-type' => 'users',
565             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}first-name'  => $contact->n_given,
566             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}last-name'   => $contact->n_family
567         );
568         
569         if (!empty(Tinebase_Core::getUser()->accountEmailAddress)) {
570             $principal['{http://sabredav.org/ns}email-address'] = $contact->email;
571         }
572         
573         return $principal;
574     }
575     
576     /**
577      * convert container grants to principals 
578      * 
579      * @param Tinebase_Record_RecordSet $containers
580      * @return array
581      * 
582      * @todo improve algorithm to fetch all contact/list_ids at once
583      */
584     protected function _containerGrantsToPrincipals(Tinebase_Record_RecordSet $containers)
585     {
586         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
587                 __METHOD__ . '::' . __LINE__ . ' Converting grants to principals for ' . count($containers) . ' containers.');
588         
589         $result = array();
590         
591         foreach ($containers as $container) {
592             $cacheId = Tinebase_Helper::convertCacheId('_containerGrantsToPrincipals' . $container->getId() . $container->seq);
593             
594             $containerPrincipals = Tinebase_Core::getCache()->load($cacheId);
595             
596             if ($containerPrincipals === false) {
597                 $containerPrincipals = array();
598                 
599                 $grants = Tinebase_Container::getInstance()->getGrantsOfContainer($container);
600                 
601                 foreach ($grants as $grant) {
602                     switch ($grant->account_type) {
603                         case 'group':
604                             $group = Tinebase_Group::getInstance()->getGroupById($grant->account_id);
605                             if ($group->list_id) {
606                                 $containerPrincipals[] = self::PREFIX_GROUPS . '/' . $group->list_id;
607                             }
608                             break;
609                             
610                         case 'user':
611                             // skip if grant belongs to the owner of the calendar
612                             if ($contact->account_id == $grant->account_id) {
613                                 continue;
614                             }
615                             $user = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $grant->account_id);
616                             if ($user->contact_id) {
617                                 $containerPrincipals[] = self::PREFIX_USERS . '/' . $user->contact_id;
618                             }
619                             
620                             break;
621                     }
622                 }
623                 
624                 Tinebase_Core::getCache()->save($containerPrincipals, $cacheId, array(), /* 1 day */ 24 * 60 * 60);
625             }
626             
627             $result = array_merge($result, $containerPrincipals);
628         }
629         
630         // users and groups can be duplicate
631         $result = array_unique($result);
632         
633         return $result;
634     }
635     
636     /**
637      * convert list model to principal array
638      * 
639      * @param Addressbook_Model_List $list
640      * @return array
641      */
642     protected function _listToPrincipal(Addressbook_Model_List $list)
643     {
644         $principal = array(
645             'uri'                     => self::PREFIX_GROUPS . '/' . $list->getId(),
646             '{DAV:}displayname'       => $list->name . ' (Group)',
647             '{DAV:}alternate-URI-set' => array('urn:uuid:' . $list->getId()),
648             
649             '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-user-type'  => 'GROUP',
650             
651             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}record-type' => 'groups',
652             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}first-name'  => 'Group',
653             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}last-name'   => $list->name,
654         );
655         
656         return $principal;
657     }
658 }