0011982: support expanded-group-member-set report
authorCornelius Weiß <c.weiss@metaways.de>
Thu, 1 Oct 2015 16:53:21 +0000 (18:53 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Fri, 8 Jul 2016 09:46:48 +0000 (11:46 +0200)
* ical searches for groups on attendee add
* if group is chose by user it tries to expand the group members
* we deliver the group members + the group itself as INTELLIGROUP so
  the group itself gets part in the event (like in tine20 web ui)

https://forge.tine20.org/view.php?id=11982

Change-Id: Ibaabd5417b8d391f2bb745a348a2173fc353c6b6
Reviewed-on: http://gerrit.tine20.com/customers/3330
Tested-by: Jenkins CI (http://ci.tine20.com/)
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
tests/tine20/Tinebase/WebDav/AllTests.php
tests/tine20/Tinebase/WebDav/Plugin/AbstractBaseTest.php
tests/tine20/Tinebase/WebDav/Plugin/ExpandedPropertiesReportTest.php [new file with mode: 0644]
tests/tine20/Tinebase/WebDav/PrincipalBackendTest.php
tine20/Calendar/Model/Attender.php
tine20/Tinebase/Server/WebDAV.php
tine20/Tinebase/WebDav/Plugin/ExpandedPropertiesReport.php [new file with mode: 0644]
tine20/Tinebase/WebDav/PrincipalBackend.php
tine20/Tinebase/WebDav/Root.php

index 000e017..27648ff 100644 (file)
@@ -23,6 +23,7 @@ class Tinebase_WebDav_AllTests
         $suite->addTestSuite('Tinebase_WebDav_Plugin_OwnCloudTest');
         $suite->addTestSuite('Tinebase_WebDav_Plugin_PrincipalSearchTest');
         $suite->addTestSuite('Tinebase_WebDav_Plugin_SyncTokenTest');
+        $suite->addTestSuite('Tinebase_WebDav_Plugin_ExpandedPropertiesReportTest');
         $suite->addTestSuite('Tinebase_WebDav_RootTest');
 
         return $suite;
index 5f004c5..5dd38fa 100644 (file)
@@ -48,6 +48,7 @@ abstract class Tinebase_WebDav_Plugin_AbstractBaseTest extends TestCase
         parent::setUp();
 
         $this->server = new Sabre\DAV\Server(new Tinebase_WebDav_Root());
+        $this->server->debugExceptions = true;
 
         $this->response = new Sabre\HTTP\ResponseMock();
         $this->server->httpResponse = $this->response;
diff --git a/tests/tine20/Tinebase/WebDav/Plugin/ExpandedPropertiesReportTest.php b/tests/tine20/Tinebase/WebDav/Plugin/ExpandedPropertiesReportTest.php
new file mode 100644 (file)
index 0000000..ddd344d
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Tine 2.0 - http://www.tine20.org
+ *
+ * @license     http://www.gnu.org/licenses/agpl.html
+ * @copyright   Copyright (c) 2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Cornelius Weiß <c.weiss@metaways.de>
+ */
+
+
+/**
+ * Test class for Tinebase_WebDav_Plugin_OwnCloud
+ */
+class Tinebase_WebDav_Plugin_ExpandedPropertiesReportTest extends Tinebase_WebDav_Plugin_PrincipalSearchTest
+{
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->server->addPlugin(new Tinebase_WebDav_Plugin_ExpandedPropertiesReport());
+    }
+
+    public function testExpandProperty()
+    {
+        $list = Tinebase_Group::getInstance()->getGroupById(Tinebase_Core::getUser()->accountPrimaryGroup);
+
+        $body = '<?xml version="1.0" encoding="UTF-8"?>
+                <A:expand-property xmlns:A="DAV:">
+                  <A:property name="expanded-group-member-set" namespace="http://calendarserver.org/ns/">
+                    <A:property name="last-name" namespace="http://calendarserver.org/ns/"/>
+                    <A:property name="principal-URL" namespace="DAV:"/>
+                    <A:property name="calendar-user-type" namespace="urn:ietf:params:xml:ns:caldav"/>
+                    <A:property name="calendar-user-address-set" namespace="urn:ietf:params:xml:ns:caldav"/>
+                    <A:property name="first-name" namespace="http://calendarserver.org/ns/"/>
+                    <A:property name="record-type" namespace="http://calendarserver.org/ns/"/>
+                    <A:property name="displayname" namespace="DAV:"/>
+                  </A:property>
+                </A:expand-property>';
+
+        $request = new Sabre\HTTP\Request(array(
+            'REQUEST_METHOD' => 'REPORT',
+            'REQUEST_URI'    => '/principals/groups/' . $list->list_id . '/'
+        ));
+        $request->setBody($body);
+
+        $this->server->httpRequest = $request;
+        $this->server->exec();
+
+        $responseDoc = new DOMDocument();
+        $responseDoc->loadXML($this->response->body);
+        #$responseDoc->formatOutput = true; echo $responseDoc->saveXML();
+        $xpath = new DomXPath($responseDoc);
+        $xpath->registerNamespace('cal', 'urn:ietf:params:xml:ns:caldav');
+        $xpath->registerNamespace('cs', 'http://calendarserver.org/ns/');
+
+        $nodes = $xpath->query('///cs:expanded-group-member-set/d:response/d:href[text()="/principals/groups/' . $list->list_id . '/"]');
+        $this->assertEquals(1, $nodes->length, 'group itself (not shown by client) is missing');
+
+        $nodes = $xpath->query('///cs:expanded-group-member-set/d:response/d:href[text()="/principals/intelligroups/' . $list->list_id . '/"]');
+        $this->assertEquals(1, $nodes->length, 'intelligroup (to keep group itself) is missing');
+
+        $nodes = $xpath->query('///cs:expanded-group-member-set/d:response/d:href[text()="/principals/users/' . Tinebase_Core::getUser()->contact_id . '/"]');
+        $this->assertEquals(1, $nodes->length, 'user is missing');
+    }
+
+    public function testConvert()
+    {
+        $emailArrayFromClient = array(array(
+            'userType'     => 'user',
+            'firstName'    => 'Users',
+            'lastName'     => '(Group)',
+            'partStat'     => 'NEEDS-ACTION',
+            'role'         => 'REQ',
+            'email'        => 'urn:uuid:principals/intelligroups/cc74c2880f8c5c0eaacc57ea95f4d2571fb8a4b1',
+        ));
+
+        $event = new Calendar_Model_Event();
+        Calendar_Model_Attender::emailsToAttendee($event, $emailArrayFromClient);
+
+        $this->assertEquals('cc74c2880f8c5c0eaacc57ea95f4d2571fb8a4b1', $event->attendee->getFirstRecord()->user_id);
+        $this->assertEquals('group', $event->attendee->getFirstRecord()->user_type);
+    }
+}
\ No newline at end of file
index 1714874..e86dbaf 100644 (file)
@@ -75,7 +75,7 @@ class Tinebase_WebDav_PrincipalBackendTest extends TestCase
         //var_dump($principal);
         
         $this->assertEquals(Tinebase_WebDav_PrincipalBackend::PREFIX_GROUPS . '/' . $list->list_id, $principal['uri']);
-        $this->assertEquals($list->name . ' (Group)', $principal['{DAV:}displayname']);
+        $this->assertEquals($list->name . ' ('. Tinebase_Translation::getTranslation('Calendar')->_('Group') . ')', $principal['{DAV:}displayname']);
     }
     
     public function testGetPrincipalByUserPath()
index 4d5a20e..246fc3e 100644 (file)
@@ -220,7 +220,12 @@ class Calendar_Model_Attender extends Tinebase_Record_Abstract
                 break;
             case self::USERTYPE_GROUP:
             case self::USERTYPE_RESOURCE:
-                return $resolvedUser->name ?: $resolvedUser->n_fileas;
+                $translation = Tinebase_Translation::getTranslation('Calendar');
+                $name = $resolvedUser->name ?: $resolvedUser->n_fileas;
+                if ($this->user_type == self::USERTYPE_GROUP) {
+                    $name . ' (' . $translation->_('Group') . ')';
+                }
+                return $name;
                 break;
             default:
                 throw new Exception("type " . $this->user_type . " not yet supported");
@@ -323,8 +328,8 @@ class Calendar_Model_Attender extends Tinebase_Record_Abstract
      */
     public static function emailsToAttendee(Calendar_Model_Event $_event, $_emails, $_implicitAddMissingContacts = TRUE)
     {
-        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
-            Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " list of new attendees " . print_r($_emails, true));
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+            Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . " list of new attendees " . print_r($_emails, true));
         
         if (! $_event->attendee instanceof Tinebase_Record_RecordSet) {
             $_event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
@@ -393,14 +398,22 @@ class Calendar_Model_Attender extends Tinebase_Record_Abstract
             $attendeeId = NULL;
             
             if ($newAttendee['userType'] == Calendar_Model_Attender::USERTYPE_USER) {
+                // list from groupmember expand
+                if ( ! $attendeeId &&
+                    preg_match('#^urn:uuid:principals/intelligroups/([a-z0-9]+)#', $newAttendee['email'], $matches)
+                ) {
+                    $newAttendee['userType'] = Calendar_Model_Attender::USERTYPE_GROUP;
+                    $attendeeId = $matches[1];
+                }
+
                 // does a contact with this email address exist?
-                if ($contact = self::resolveEmailToContact($newAttendee, false)) {
+                if (! $attendeeId && $contact = self::resolveEmailToContact($newAttendee, false)) {
                     $attendeeId = $contact->getId();
                     
                 }
                 
                 // does a resouce with this email address exist?
-                if ( ! $attendeeId) {
+                if (! $attendeeId) {
                     $resources = Calendar_Controller_Resource::getInstance()->search(new Calendar_Model_ResourceFilter(array(
                         array('field' => 'email', 'operator' => 'equals', 'value' => $newAttendee['email']),
                     )));
@@ -428,8 +441,8 @@ class Calendar_Model_Attender extends Tinebase_Record_Abstract
                         $newAttendee['userType'] = Calendar_Model_Attender::USERTYPE_GROUP;
                         $attendeeId = $lists->getFirstRecord()->group_id;
                     }
-                } 
-                
+                }
+
                 if (! $attendeeId) {
                     // autocreate a contact if allowed
                     $contact = self::resolveEmailToContact($newAttendee, $_implicitAddMissingContacts);
@@ -437,6 +450,7 @@ class Calendar_Model_Attender extends Tinebase_Record_Abstract
                         $attendeeId = $contact->getId();
                     }
                 }
+
             } else if($newAttendee['userType'] == Calendar_Model_Attender::USERTYPE_GROUP) {
                 $lists = Addressbook_Controller_List::getInstance()->search(new Addressbook_Model_ListFilter(array(
                     array('field' => 'name',       'operator' => 'equals', 'value' => $newAttendee['displayName']),
@@ -450,7 +464,7 @@ class Calendar_Model_Attender extends Tinebase_Record_Abstract
                     $attendeeId = $lists->getFirstRecord()->group_id;
                 }
             }
-            
+
             if ($attendeeId !== NULL) {
                 // finally add to attendee
                 $_event->attendee->addRecord(new Calendar_Model_Attender(array(
index 68b06cc..e4e469d 100644 (file)
@@ -108,7 +108,7 @@ class Tinebase_Server_WebDAV extends Tinebase_Server_Abstract implements Tinebas
         
         $aclPlugin = new \Sabre\DAVACL\Plugin();
         $aclPlugin->defaultUsernamePath    = Tinebase_WebDav_PrincipalBackend::PREFIX_USERS;
-        $aclPlugin->principalCollectionSet = array (Tinebase_WebDav_PrincipalBackend::PREFIX_USERS, Tinebase_WebDav_PrincipalBackend::PREFIX_GROUPS);
+        $aclPlugin->principalCollectionSet = array (Tinebase_WebDav_PrincipalBackend::PREFIX_USERS, Tinebase_WebDav_PrincipalBackend::PREFIX_GROUPS, Tinebase_WebDav_PrincipalBackend::PREFIX_INTELLIGROUPS);
         
         $aclPlugin->principalSearchPropertySet = array(
             '{DAV:}displayname'                                                   => 'Display name',
@@ -133,6 +133,7 @@ class Tinebase_Server_WebDAV extends Tinebase_Server_Abstract implements Tinebas
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_Inverse());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_OwnCloud());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_PrincipalSearch());
+        self::$_server->addPlugin(new Tinebase_WebDav_Plugin_ExpandedPropertiesReport());
         self::$_server->addPlugin(new \Sabre\DAV\Browser\Plugin());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_SyncToken());
 
diff --git a/tine20/Tinebase/WebDav/Plugin/ExpandedPropertiesReport.php b/tine20/Tinebase/WebDav/Plugin/ExpandedPropertiesReport.php
new file mode 100644 (file)
index 0000000..84c472f
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * CalDAV plugin for expanded-group-member-set
+ *
+ * NOTE: for expand-property reports some properties seem to be prefixed with 'expanded-':
+ * - expanded-group-member-set
+ * - expanded-group-membership
+ *
+ * It's not clear if this is according to the standards, but iCal sends this requests and
+ * Sabre can't cope with it yet
+ *
+ * @copyright  Copyright (c) 2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author     Cornelius Weiß <c.weiss@metaways.de>
+ * @license    http://www.gnu.org/licenses/agpl.html
+ */
+class Tinebase_WebDav_Plugin_ExpandedPropertiesReport extends \Sabre\DAV\ServerPlugin {
+
+    /**
+     * Reference to server object
+     *
+     * @var \Sabre\DAV\Server
+     */
+    protected $server;
+
+    /**
+     * Returns a list of features for the DAV: HTTP header. 
+     * 
+     * @return array 
+     */
+    public function getFeatures() 
+    {
+        return array();
+    }
+
+    /**
+     * (non-PHPdoc)
+     * @see \Sabre\DAV\ServerPlugin::getPluginName()
+     */
+    public function getPluginName() 
+    {
+        return 'expandPropertiesReport';
+    }
+    
+    /**
+     * (non-PHPdoc)
+     * @see \Sabre\DAV\ServerPlugin::getSupportedReportSet()
+     */
+    public function getSupportedReportSet($uri) 
+    {
+        return array(
+            '{DAV:}expand-property',
+        );
+
+    }
+    /**
+     * Initializes the plugin 
+     * 
+     * @param \Sabre\DAV\Server $server 
+     * @return void
+     */
+    public function initialize(\Sabre\DAV\Server $server) 
+    {
+        $this->server = $server;
+
+        $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties'));
+    }
+    
+    /**
+     * beforeGetProperties
+     *
+     * This method handler is invoked before any after properties for a
+     * resource are fetched. This allows us to add in any CalDAV specific
+     * properties.
+     *
+     * @param string $path
+     * @param DAV\INode $node
+     * @param array $requestedProperties
+     * @param array $returnedProperties
+     * @return void
+     */
+    public function beforeGetProperties($path, \Sabre\DAV\INode $node, &$requestedProperties, &$returnedProperties)
+    {
+        if (in_array('{http://calendarserver.org/ns/}expanded-group-member-set', $requestedProperties)) {
+            $parentNode = $this->server->tree->getNodeForPath($path);
+            $groupMemberSet = $parentNode->getGroupMemberSet();
+
+            // iCal want's to have the group itself in the response set
+            $groupMemberSet[] = $path;
+
+            // have record for group itself
+            $groupMemberSet[] = str_replace(
+                Tinebase_WebDav_PrincipalBackend::PREFIX_GROUPS,
+                Tinebase_WebDav_PrincipalBackend::PREFIX_INTELLIGROUPS,
+                $path
+            );
+
+            $returnedProperties[200]['{http://calendarserver.org/ns/}expanded-group-member-set'] = new Sabre\DAV\Property\HrefList($groupMemberSet);
+        }
+    }
+
+}
index 08a259e..4a9700e 100644 (file)
@@ -19,6 +19,7 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
 {
     const PREFIX_USERS  = 'principals/users';
     const PREFIX_GROUPS = 'principals/groups';
+    const PREFIX_INTELLIGROUPS = 'principals/intelligroups';
     const SHARED        = 'shared';
     
     /**
@@ -31,6 +32,7 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
         
         switch ($prefixPath) {
             case self::PREFIX_GROUPS:
+            case self::PREFIX_INTELLIGROUPS:
                 $filter = new Addressbook_Model_ListFilter(array(
                     array(
                         'field'     => 'type',
@@ -42,7 +44,7 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
                 $lists = Addressbook_Controller_List::getInstance()->search($filter);
                 
                 foreach ($lists as $list) {
-                    $principals[] = $this->_listToPrincipal($list);
+                    $principals[] = $this->_listToPrincipal($list, $prefixPath);
                 }
                 
                 break;
@@ -119,6 +121,7 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
                 break;
                 
             case self::PREFIX_GROUPS:
+            case self::PREFIX_INTELLIGROUPS:
                 $filter = new Addressbook_Model_ListFilter(array(
                     array(
                         'field'     => 'type',
@@ -140,7 +143,7 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
                     return null;
                 }
                 
-                $principal = $this->_listToPrincipal($list);
+                $principal = $this->_listToPrincipal($list, $prefix);
                 
                 break;
                 
@@ -282,6 +285,7 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
                 break;
                 
             case self::PREFIX_GROUPS:
+            case self::PREFIX_INTELLIGROUPS:
                 $filter = new Addressbook_Model_ListFilter(array(
                     array(
                         'field'     => 'type',
@@ -434,6 +438,7 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
         
         switch ($prefixPath) {
             case self::PREFIX_GROUPS:
+            case self::PREFIX_INTELLIGROUPS:
                 $filter = new Addressbook_Model_ListFilter(array(
                     array(
                         'field'     => 'type',
@@ -637,22 +642,30 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
      * convert list model to principal array
      * 
      * @param Addressbook_Model_List $list
+     * @param string $prefix
      * @return array
      */
-    protected function _listToPrincipal(Addressbook_Model_List $list)
+    protected function _listToPrincipal(Addressbook_Model_List $list, $prefix)
     {
+        $calUserType = $prefix == self::PREFIX_INTELLIGROUPS ? 'INTELLIGROUP' : 'GROUP';
+
         $principal = array(
-            'uri'                     => self::PREFIX_GROUPS . '/' . $list->getId(),
-            '{DAV:}displayname'       => $list->name . ' (Group)',
-            '{DAV:}alternate-URI-set' => array('urn:uuid:' . $list->getId()),
+            'uri'                     => $prefix . '/' . $list->getId(),
+            '{DAV:}displayname'       => $list->name . ' (' . $translation = Tinebase_Translation::getTranslation('Calendar')->_('Group') . ')',
+            '{DAV:}alternate-URI-set' => array('urn:uuid:' . $prefix . '/' . $list->getId()),
             
-            '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-user-type'  => 'GROUP',
+            '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-user-type'  => $calUserType,
             
             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}record-type' => 'groups',
-            '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}first-name'  => 'Group',
+            '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}first-name'  => Tinebase_Translation::getTranslation('Calendar')->_('Group'),
             '{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}last-name'   => $list->name,
         );
-        
+
+        if ($calUserType == 'INTELLIGROUP') {
+            // OSX needs an email adress to send the attendee
+            $principal['{http://sabredav.org/ns}email-address'] = 'urn:uuid:' . $prefix . '/' . $list->getId();
+        }
+
         return $principal;
     }
 }
index 9fe395e..56d1cef 100644 (file)
@@ -26,7 +26,8 @@ class Tinebase_WebDav_Root extends \Sabre\DAV\SimpleCollection
         parent::__construct('root', array(
             new \Sabre\DAV\SimpleCollection('principals', array(
                 new Tinebase_WebDav_PrincipalCollection(new Tinebase_WebDav_PrincipalBackend(), Tinebase_WebDav_PrincipalBackend::PREFIX_USERS),
-                new Tinebase_WebDav_PrincipalCollection(new Tinebase_WebDav_PrincipalBackend(), Tinebase_WebDav_PrincipalBackend::PREFIX_GROUPS)
+                new Tinebase_WebDav_PrincipalCollection(new Tinebase_WebDav_PrincipalBackend(), Tinebase_WebDav_PrincipalBackend::PREFIX_GROUPS),
+                new Tinebase_WebDav_PrincipalCollection(new Tinebase_WebDav_PrincipalBackend(), Tinebase_WebDav_PrincipalBackend::PREFIX_INTELLIGROUPS)
             ))
         ));