implement iCal managed attachments
authorCornelius Weiß <mail@corneliusweiss.de>
Tue, 15 Apr 2014 14:06:46 +0000 (16:06 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Thu, 4 Sep 2014 09:26:32 +0000 (11:26 +0200)
see http://tools.ietf.org/html/draft-daboo-caldav-attachments-03

NOTE: the implementation just reflects the current support of the
      iCal client. see limitations in the comments

Change-Id: Ic75f7a0c3a11c3be4499d29adffbbfa28a4fefc6
Reviewed-on: http://gerrit.tine20.com/customers/349
Tested-by: Jenkins CI (http://ci.tine20.com/)
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
tests/tine20/Calendar/Frontend/CalDAV/AllTests.php
tests/tine20/Calendar/Frontend/CalDAV/PluginManagedAttachmentsTest.php [new file with mode: 0644]
tests/tine20/Calendar/Frontend/WebDAV/EventTest.php
tine20/Calendar/Controller/MSEventFacade.php
tine20/Calendar/Convert/Event/VCalendar/Abstract.php
tine20/Calendar/Convert/Event/VCalendar/MacOSX.php
tine20/Calendar/Frontend/CalDAV/PluginManagedAttachments.php [new file with mode: 0644]
tine20/Calendar/Frontend/WebDAV/Event.php
tine20/Tinebase/FileSystem.php
tine20/Tinebase/Server/WebDAV.php

index 48725b5..29a5c9b 100644 (file)
@@ -25,6 +25,7 @@ class Calendar_Frontend_CalDAV_AllTests
         $suite = new PHPUnit_Framework_TestSuite('Tine 2.0 Calendar All Frontend CalDAV Tests');
         $suite->addTestSuite('Calendar_Frontend_CalDAV_PluginDefaultAlarmsTest');
         $suite->addTestSuite('Calendar_Frontend_CalDAV_ProxyTest');
+        $suite->addTestSuite('Calendar_Frontend_CalDAV_PluginManagedAttachmentsTest');
         return $suite;
     }
 }
diff --git a/tests/tine20/Calendar/Frontend/CalDAV/PluginManagedAttachmentsTest.php b/tests/tine20/Calendar/Frontend/CalDAV/PluginManagedAttachmentsTest.php
new file mode 100644 (file)
index 0000000..0ac0ed7
--- /dev/null
@@ -0,0 +1,213 @@
+<?php
+/**
+ * Tine 2.0 - http://www.tine20.org
+ * 
+ * @package     Calendar
+ * @subpackage  CalDAV
+ * @license     http://www.gnu.org/licenses/agpl.html
+ * @copyright   Copyright (c) 2013-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Cornelius Weiss <c.weiss@metaways.de>
+ */
+
+/**
+ * Test helper
+ */
+require_once __DIR__ . '/../../../../../tine20/vendor/sabre/dav/tests/Sabre/HTTP/ResponseMock.php';
+
+/**
+ * Test class for Calendar_Frontend_CalDAV_PluginManagedAttachments
+ */
+class Calendar_Frontend_CalDAV_PluginManagedAttachmentsTest extends TestCase
+{
+    /**
+     * 
+     * @var Sabre\DAV\Server
+     */
+    protected $server;
+    
+    /**
+     * Sets up the fixture.
+     * This method is called before a test is executed.
+     *
+     * @access protected
+     */
+    public function setUp()
+    {
+        $this->calDAVTests = new Calendar_Frontend_WebDAV_EventTest();
+        $this->calDAVTests->setup();
+        
+        parent::setUp();
+        
+        $this->hostname = 'tine.example.com';
+        $this->originalHostname = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null;
+        $_SERVER['HTTP_HOST'] = $this->hostname;
+        
+        $this->server = new Sabre\DAV\Server(new Tinebase_WebDav_Root());
+        
+        $this->plugin = new Calendar_Frontend_CalDAV_PluginManagedAttachments();
+        
+        $this->server->addPlugin($this->plugin);
+        
+        
+        $this->response = new Sabre\HTTP\ResponseMock();
+        $this->server->httpResponse = $this->response;
+    }
+
+    public function tearDown()
+    {
+        if ($this->originalHostname) {
+            $_SERVER['HTTP_HOST'] = $this->originalHostname;
+        }
+        $this->calDAVTests->tearDown();
+    }
+    
+    /**
+     * test getPluginName method
+     */
+    public function testGetPluginName()
+    {
+        $pluginName = $this->plugin->getPluginName();
+        
+        $this->assertEquals('calendarManagedAttachments', $pluginName);
+    }
+    
+    /**
+     * test testAddAttachment 
+     */
+    public function testAddAttachment()
+    {
+        $event = $this->calDAVTests->testCreateEventWithInternalOrganizer();
+        
+        $request = new Sabre\HTTP\Request(array(
+            'HTTP_CONTENT_TYPE' => 'text/plain',
+            'HTTP_CONTENT_DISPOSITION' => 'attachment;filename="agenda.txt"',
+            'REQUEST_METHOD' => 'POST',
+            'REQUEST_URI'    => '/calendars/' . 
+                Tinebase_Core::getUser()->contact_id . '/'. 
+                $event->getRecord()->container_id . '/' .
+                $event->getRecord()->uid . '.ics?action=attachment-add',
+            'QUERY_STRING'   => 'action=attachment-add',
+            'HTTP_DEPTH'     => '0',
+        ));
+        
+        $agenda = 'HELLO WORLD';
+        $request->setBody($agenda);
+
+        $this->server->httpRequest = $request;
+        $this->server->exec();
+        
+        $vcalendar = stream_get_contents($this->response->body);
+//         echo $vcalendar;
+        
+        $attachments = Tinebase_FileSystem_RecordAttachments::getInstance()
+        ->getRecordAttachments($event->getRecord());
+        
+        $this->assertEquals('HTTP/1.1 201 Created', $this->response->status);
+        $this->assertContains('ATTACH;MANAGED-ID='. sha1($agenda), $vcalendar, $vcalendar);
+        $this->assertEquals(1, $attachments->count());
+        $this->assertEquals('agenda.txt', $attachments[0]->name);
+    }
+    
+    /**
+     * test testOverwriteAttachment
+     * 
+     * NOTE: current iCal can not update, but re-adds/overwrides if
+     *       you drag and drop an attachment twice
+     */
+    public function testOverwriteAttachment()
+    {
+        $event = $this->calDAVTests->createEventWithAttachment();
+        
+        $request = new Sabre\HTTP\Request(array(
+                'HTTP_CONTENT_TYPE' => 'text/plain',
+                'HTTP_CONTENT_DISPOSITION' => 'attachment;filename="agenda.html"',
+                'REQUEST_METHOD' => 'POST',
+                'REQUEST_URI'    => '/calendars/' .
+                Tinebase_Core::getUser()->contact_id . '/'.
+                $event->getRecord()->container_id . '/' .
+                $event->getRecord()->uid . '.ics?action=attachment-add',
+                'QUERY_STRING'   => 'action=attachment-add',
+                'HTTP_DEPTH'     => '0',
+        ));
+        
+        $agenda = 'GODDBYE WORLD';
+        $request->setBody($agenda);
+        
+        $this->server->httpRequest = $request;
+        $this->server->exec();
+        
+        $vcalendar = stream_get_contents($this->response->body);
+        //         echo $vcalendar;
+        
+        $attachments = Tinebase_FileSystem_RecordAttachments::getInstance()
+        ->getRecordAttachments($event->getRecord());
+        
+        $this->assertEquals('HTTP/1.1 201 Created', $this->response->status);
+        $this->assertContains('ATTACH;MANAGED-ID='. sha1($agenda), $vcalendar, $vcalendar);
+        $this->assertEquals(1, $attachments->count());
+        $this->assertEquals('agenda.html', $attachments[0]->name);
+    }
+    
+    public function testUpdateAttachment()
+    {
+        $event = $this->calDAVTests->createEventWithAttachment();
+        $attachmentNode = $event->getRecord()->attachments->getFirstRecord();
+        
+        $request = new Sabre\HTTP\Request(array(
+            'HTTP_CONTENT_TYPE' => 'text/plain',
+            'HTTP_CONTENT_DISPOSITION' => 'attachment;filename=agenda.txt',
+            'REQUEST_METHOD' => 'POST',
+            'REQUEST_URI'    => '/calendars/' .
+            Tinebase_Core::getUser()->contact_id . '/'.
+            $event->getRecord()->container_id . '/' .
+            $event->getRecord()->uid . '.ics?action=attachment-update&managed-id='.$attachmentNode->hash,
+            'QUERY_STRING'   => 'action=attachment-update&managed-id='.$attachmentNode->hash,
+            'HTTP_DEPTH'     => '0',
+        ));
+        
+        $agenda = 'GODDBYE WORLD';
+        $request->setBody($agenda);
+        
+        $this->server->httpRequest = $request;
+        $this->server->exec();
+        
+        $vcalendar = stream_get_contents($this->response->body);
+//         echo $vcalendar;
+        
+//         $this->assertEquals('HTTP/1.1 201 Created', $this->response->status);
+        $this->assertContains('ATTACH;MANAGED-ID='. sha1($agenda), $vcalendar, $vcalendar);
+         $this->assertNotContains($attachmentNode->hash, $vcalendar, 'old managed-id');
+        //@TODO assert URI
+        //@TODO /fetch attachement & assert contents
+    }
+    
+    public function testRemoveAttachment()
+    {
+        $event = $this->calDAVTests->createEventWithAttachment();
+        $attachmentNode = $event->getRecord()->attachments->getFirstRecord();
+        
+        $request = new Sabre\HTTP\Request(array(
+                'HTTP_CONTENT_TYPE' => 'text/plain',
+                'HTTP_CONTENT_DISPOSITION' => 'attachment;filename=agenda.txt',
+                'REQUEST_METHOD' => 'POST',
+                'REQUEST_URI'    => '/calendars/' .
+                Tinebase_Core::getUser()->contact_id . '/'.
+                $event->getRecord()->container_id . '/' .
+                $event->getRecord()->uid . '.ics?action=attachment-remove&managed-id='.$attachmentNode->hash,
+                'QUERY_STRING'   => 'action=attachment-remove&managed-id='.$attachmentNode->hash,
+                'HTTP_DEPTH'     => '0',
+        ));
+        
+        $this->server->httpRequest = $request;
+        $this->server->exec();
+        
+        $vcalendar = stream_get_contents($this->response->body);
+//         echo $vcalendar;
+        
+//         $this->assertEquals('HTTP/1.1 204 No Content', $this->response->status);
+        $this->assertNotContains('ATTACH;MANAGED-ID=', $vcalendar, $vcalendar);
+        
+        $attachments = Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachments($event->getRecord());
+        $this->assertEquals(0, $attachments->count());
+    }
+}
index 439a2e8..3f1f652 100644 (file)
@@ -420,6 +420,27 @@ class Calendar_Frontend_WebDAV_EventTest extends Calendar_TestCase
     }
     
     /**
+     * test deleting attachment from existing event
+     */
+    public function testDeleteAttachment()
+    {
+        $_SERVER['HTTP_USER_AGENT'] = 'CalendarStore/5.0 (1127); iCal/5.0 (1535); Mac OS X/10.7.1 (11B26)';
+        
+        $event = $this->createEventWithAttachment(2);
+        
+        // remove agenda.html
+        $clone = clone $event;
+        $attachments = $clone->getRecord()->attachments;
+        $attachments->removeRecord($attachments->filter('name', 'agenda.html')->getFirstRecord());
+        $event->put($clone->get());
+        
+        // assert agenda2.html exists
+        $record = $event->getRecord();
+        $this->assertEquals(1, $record->attachments->count());
+        $this->assertEquals('agenda2.html', $record->attachments->getFirstRecord()->name);
+    }
+    
+    /**
      * test updating existing event
      */
     public function testPutEventFromGenericClient()
@@ -831,4 +852,29 @@ class Calendar_Frontend_WebDAV_EventTest extends Calendar_TestCase
         $this->assertNotNull($currentUser, 'currentUser not found in attendee');
         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, $currentUser->status, print_r($currentUser->toArray(), true));
     }
+    
+    /**
+     * create event with attachment
+     *
+     * @return multitype:Ambigous <Calendar_Frontend_WebDAV_Event, Calendar_Frontend_WebDAV_Event> Ambigous <Tinebase_Model_Tree_Node, Tinebase_Record_Interface, Tinebase_Record_Abstract, NULL, unknown>
+     */
+    public function createEventWithAttachment($count=1)
+    {
+        $event = $this->testCreateEventWithInternalOrganizer();
+        
+        for ($i=1; $i<=$count; $i++) {
+            $suffix = $i>1 ? $i : '';
+            
+            $agenda = fopen("php://temp", 'r+');
+            fputs($agenda, "HELLO WORLD$suffix");
+            rewind($agenda);
+        
+            $attachmentController = Tinebase_FileSystem_RecordAttachments::getInstance();
+            $attachmentNode = $attachmentController->addRecordAttachment($event->getRecord(), "agenda{$suffix}.html", $agenda);
+        }
+    
+        $event = new Calendar_Frontend_WebDAV_Event($event->getContainer(), $event->getRecord()->getId());
+    
+        return $event;
+    }
 }
index 791bddc..b3dfafc 100644 (file)
@@ -579,6 +579,7 @@ class Calendar_Controller_MSEventFacade implements Tinebase_Controller_Record_In
     protected function _toiTIP($_event)
     {
         if ($_event instanceof Tinebase_Record_RecordSet) {
+            Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($_event);
             foreach ($_event as $idx => $event) {
                 try {
                     $_event[$idx] = $this->_toiTIP($event);
@@ -591,12 +592,15 @@ class Calendar_Controller_MSEventFacade implements Tinebase_Controller_Record_In
             }
             
             return $_event;
+        } else if ($_event->is_deleted == 0) {
+            Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachments($_event);
         }
         
         // get exdates
         if ($_event->getId() && $_event->rrule) {
             $_event->exdate = $this->_eventController->getRecurExceptions($_event, TRUE, $this->getEventFilter());
             $this->getAlarms($_event);
+            Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($_event->exdate->filter('is_deleted', 0));
             
             foreach ($_event->exdate as $exdate) {
                 $this->_toiTIP($exdate);
index 8d8bc03..58f1a08 100644 (file)
@@ -299,6 +299,22 @@ class Calendar_Convert_Event_VCalendar_Abstract implements Tinebase_Convert_Inte
             }
         }
         
+        $baseUrl = Tinebase_Core::getHostname() . "/webdav/Calendar/records/Calendar_Model_Event/{$event->getId()}/";
+        
+        if ($event->attachments instanceof Tinebase_Record_RecordSet) {
+            foreach ($event->attachments as $attachment) {
+                $attach = $vcalendar->createProperty('ATTACH', "{$baseUrl}{$attachment->name}", array(
+                    'MANAGED-ID' => $attachment->hash,
+                    'FMTTYPE'    => $attachment->contenttype,
+                    'SIZE'       => $attachment->size,
+                    'FILENAME'   => $attachment->name
+                ), 'TEXT');
+                
+                $vevent->add($attach);
+            }
+            
+        }
+        
         $vcalendar->add($vevent);
     }
     
@@ -488,6 +504,16 @@ class Calendar_Convert_Event_VCalendar_Abstract implements Tinebase_Convert_Inte
     }
     
     /**
+     * template method
+     * 
+     * implement if client has support for sending attachments
+     * 
+     * @param Calendar_Model_Event          $event
+     * @param Tinebase_Record_RecordSet     $attachments
+     */
+    protected function _manageAttachmentsFromClient($event, $attachments) {}
+    
+    /**
      * convert VCALENDAR to Tinebase_Record_RecordSet of Calendar_Model_Event
      * 
      * @param  mixed  $blob  the vcalendar to parse
@@ -721,6 +747,7 @@ class Calendar_Convert_Event_VCalendar_Abstract implements Tinebase_Convert_Inte
             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' vevent ' . $vevent->serialize());
         
         $newAttendees = array();
+        $attachments = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
         $event->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
         
         foreach ($vevent->children() as $property) {
@@ -924,6 +951,29 @@ class Calendar_Convert_Event_VCalendar_Abstract implements Tinebase_Convert_Inte
                     }
                     break;
                     
+                case 'ATTACH':
+                    $name = $property['FILENAME'];
+                    
+                    $managedId = $property['MANAGED-ID'];
+                    if ($managedId) {
+                        $attachment = $event->attachments->filter('hash', $property['MANAGED-ID'])->getFirstRecord();
+                        
+                        // client reuses managed id to add attachment
+                        if (! $attachment) {
+//                             @TODO: implement
+//                             Tinebase_FileSystem_RecordAttachments::getInstance()
+//                                 ->addRecordAttachment($event, $name, $attachment);
+                        }
+                        
+                        $attachments->addRecord($attachment);
+                    }
+                    
+                    // base64
+                    else {
+                        // @TODO: implement (check if add / update / update is needed)
+                    }
+                    break;
+                    
                 case 'X-MOZ-LASTACK':
                     $lastAck = $this->_convertToTinebaseDateTime($property);
                     break;
@@ -962,6 +1012,8 @@ class Calendar_Convert_Event_VCalendar_Abstract implements Tinebase_Convert_Inte
             $event->class = Calendar_Model_Event::CLASS_PUBLIC;
         }
         
+        $this->_manageAttachmentsFromClient($event, $attachments);
+        
         if (empty($event->dtend)) {
             // TODO find out duration (see TRIGGER DURATION)
 //             if (isset($vevent->DURATION)) {
index 4b1dbd6..930046c 100644 (file)
@@ -42,7 +42,7 @@ class Calendar_Convert_Event_VCalendar_MacOSX extends Calendar_Convert_Event_VCa
         'recurid',
         'is_all_day_event',
         #'rrule_until',
-        'originator_tz'
+        'originator_tz',
     );
     
     /**
@@ -67,4 +67,15 @@ class Calendar_Convert_Event_VCalendar_MacOSX extends Calendar_Convert_Event_VCa
         
         return $newAttendee;
     }
+    
+    /**
+     * iCal supports manged attachments
+     *
+     * @param Calendar_Model_Event          $event
+     * @param Tinebase_Record_RecordSet     $attachments
+     */
+    protected function _manageAttachmentsFromClient($event, $attachments)
+    {
+        $event->attachments = $attachments;
+    }
 }
diff --git a/tine20/Calendar/Frontend/CalDAV/PluginManagedAttachments.php b/tine20/Calendar/Frontend/CalDAV/PluginManagedAttachments.php
new file mode 100644 (file)
index 0000000..c3bf60e
--- /dev/null
@@ -0,0 +1,262 @@
+<?php
+/**
+ * CalDAV plugin for draft-daboo-caldav-attachments-03
+ * 
+ * see: http://tools.ietf.org/html/draft-daboo-caldav-attachments-03
+ * 
+ * NOTE: At the moment Apple's iCal clients seem to support only a small subset of the spec:
+ * - reusing managed attachments is not used
+ * - deleting is done by PUT and not via managed-remove
+ * - client does not update files
+ * - client can not cope with recurring exceptions. It always acts on the whole serices and all exceptions
+ * 
+ * @TODO
+ * evaluate "return=representation" header
+ * add attachments via PUT with managed ID
+ *
+ * @package    Sabre
+ * @subpackage CalDAV
+ * @copyright  Copyright (c) 2014-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author     Cornelius Weiss <c.weiss@metaways.de>
+ * @license    http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Calendar_Frontend_CalDAV_PluginManagedAttachments 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('calendar-managed-attachments');
+    }
+
+    /**
+     * Returns a plugin name.
+     * 
+     * Using this name other plugins will be able to access other plugins
+     * using \Sabre\DAV\Server::getPlugin 
+     * 
+     * @return string 
+     */
+    public function getPluginName() 
+    {
+        return 'calendarManagedAttachments';
+    }
+
+    /**
+     * Initializes the plugin 
+     * 
+     * @param \Sabre\DAV\Server $server 
+     * @return void
+     */
+    public function initialize(\Sabre\DAV\Server $server) 
+    {
+        $this->server = $server;
+
+        $this->server->subscribeEvent('unknownMethod',array($this,'httpPOSTHandler'));
+    }
+    
+    /**
+     * Handles POST requests
+     *
+     * @param string $method
+     * @param string $uri
+     * @return bool
+     */
+    public function httpPOSTHandler($method, $uri) 
+    {
+        if ($method != 'POST') {
+            return;
+        }
+        
+        $getVars = array();
+        parse_str($this->server->httpRequest->getQueryString(), $getVars);
+        
+        if (!isset($getVars['action']) || !in_array($getVars['action'], 
+                array('attachment-add', 'attachment-update', 'attachment-remove'))) {
+            return;
+        }
+        
+        try {
+            $node = $this->server->tree->getNodeForPath($uri);
+        } catch (DAV\Exception\NotFound $e) {
+            // We're simply stopping when the file isn't found to not interfere
+            // with other plugins.
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
+                Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ .
+                " did not find node -> stopping");
+            }
+            return;
+        }
+        
+        if (!$node instanceof Calendar_Frontend_WebDAV_Event) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
+                Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . 
+                    " node is no event -> stopping ");
+            }
+            return;
+        }
+        
+        $name = 'NO NAME';
+        $disposition = $this->server->httpRequest->getHeader('Content-Disposition');
+        $contentType = $this->server->httpRequest->getHeader('Content-Type');
+        $managedId = isset($getVars['managed-id']) ? $getVars['managed-id'] : NULL;
+        $rid = $this->getRecurranceIds($getVars);
+        list($contentType) = explode(';', $contentType);
+        if (preg_match('/filename=(.*)[ ;]{0,1}/', $disposition, $matches)) {
+            $name = trim($matches[1], " \t\n\r\0\x0B\"'");
+        }
+        
+        // NOTE inputstream can not be rewinded
+        $inputStream = fopen('php://temp','r+');
+        stream_copy_to_stream($this->server->httpRequest->getBody(), $inputStream);
+        rewind($inputStream);
+        
+        list ($attachmentId) = Tinebase_FileSystem::getInstance()->createFileBlob($inputStream);
+        
+        switch ($getVars['action']) {
+            case 'attachment-add':
+                
+                $attachment = new Tinebase_Model_Tree_Node(array(
+                    'name'         => $name,
+                    'type'         => Tinebase_Model_Tree_Node::TYPE_FILE,
+                    'contenttype'  => $contentType,
+                    'hash'         => $attachmentId,
+                ), true);
+                
+                $this->_iterateByRid($node->getRecord(), $rid, function($event) use ($name, $attachment) {
+                    $existingAttachment = $event->attachments->filter('name', $name)->getFirstRecord();
+                    if ($existingAttachment) {
+                        // yes, ... iCal does this :-(
+                        $existingAttachment->hash = $attachment->hash;
+                    }
+                    
+                    else {
+                        $event->attachments->addRecord(clone $attachment);
+                    }
+                });
+                
+                $node->update($node->getRecord());
+                
+                break;
+                
+            case 'attachment-update':
+                $eventsToUpdate = array();
+                // NOTE: iterate base & all exceptions @see 3.5.2c of spec
+                $this->_iterateByRid($node->getRecord(), NULL, function($event) use ($managedId, $attachmentId, &$eventsToUpdate) {
+                    $attachmentToUpdate = $event->attachments->filter('hash', $managedId)->getFirstRecord();
+                    if ($attachmentToUpdate) {
+                        $eventsToUpdate[] = $event;
+                        $attachmentToUpdate->hash = $attachmentId;
+                    }
+                });
+                
+                if (! $eventsToUpdate) {
+                    throw new Sabre\DAV\Exception\PreconditionFailed("no attachment with id $managedId found");
+                }
+                
+                $node->update($node->getRecord());
+                break;
+                
+            case 'attachment-remove':
+                $eventsToUpdate = array();
+                $this->_iterateByRid($node->getRecord(), $rid, function($event) use ($managedId, &$eventsToUpdate) {
+                    $attachmentToDelete = $event->attachments->filter('hash', $managedId)->getFirstRecord();
+                    if ($attachmentToDelete) {
+                        $eventsToUpdate[] = $event;
+                        $event->attachments->removeRecord($attachmentToDelete);
+                    }
+                });
+                
+                if (! $eventsToUpdate) {
+                    throw new Sabre\DAV\Exception\PreconditionFailed("no attachment with id $managedId found");
+                }
+                    
+                $node->update($node->getRecord());
+                break;
+        }
+        
+//         @TODO respect Prefer header
+        $this->server->httpResponse->setHeader('Content-Type', 'text/calendar; charset="utf-8"');
+        $this->server->httpResponse->setHeader('Content-Length', $node->getSize());
+        $this->server->httpResponse->setHeader('ETag',           $node->getETag());
+        if ($getVars['action'] != 'attachment-remove') {
+            $this->server->httpResponse->setHeader('Cal-Managed-ID', $attachmentId);
+        }
+        
+        // only at create!
+        $this->server->httpResponse->sendStatus(201);
+        $this->server->httpResponse->sendBody($node->get());
+        
+        return false;
+
+    }
+    
+    /**
+     * calls method with each event matching given rid
+     * 
+     * breaks if method returns false
+     * 
+     * @param  Calendar_Model_Event $event
+     * @param  array $rid
+     * @param  Function $method
+     * @return Tinebase_Record_RecordSet affectedEvents
+     */
+    protected function _iterateByRid($event, $rid, $method)
+    {
+        $affectedEvents = new Tinebase_Record_RecordSet('Calendar_Model_Event');
+        
+        if (! $rid || in_array('M', $rid)) {
+            $affectedEvents->addRecord($event);
+        }
+        
+        if ($event->exceptions instanceof Tinebase_Record_RecordSet) {
+            foreach($event->exceptions as $exception) {
+                if (! $rid /*|| $exception->recurid ...*/) {
+                    $affectedEvents->addRecord($exception);
+                }
+            }
+        }
+        foreach($affectedEvents as $record) {
+            if ($method($record) == false) break;
+        }
+        
+        return $affectedEvents;
+    }
+    
+    /**
+     * returns recurrance ids
+     * 
+     * NOTE: 
+     *  no rid means base & all exceptions
+     *  M means base 
+     *  specific dates point to the corresponding exceptions of course
+     *  
+     * @return array
+     */
+    public function getRecurranceIds($getVars)
+    {
+        $recurids = array();
+        
+        if (isset($getVars['rid'])) {
+            foreach ( explode(',', $getVars['rid']) as $recurid) {
+                if ($recurid) {
+                    $recurids[] = strtoupper($recurid);
+                }
+            }
+        }
+        
+        return $recurids;
+    }
+    
+}
index 0ebd34b..a8e1aff 100644 (file)
@@ -69,6 +69,38 @@ class Calendar_Frontend_WebDAV_Event extends Sabre\DAV\File implements Sabre\Cal
     }
     
     /**
+     * add attachment to event
+     * 
+     * @param string $name
+     * @param string $contentType
+     * @param stream $attachment
+     * @return string  id of attachment
+     */
+    public function addAttachment($rid, $name, $contentType, $attachment)
+    {
+        $record = $this->getRecord();
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
+            Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . 
+                " add attachment $name ($contentType) to event {$record->getId()}");
+        }
+        
+        $node = new Tinebase_Model_Tree_Node(array(
+            'name'         => $name,
+            'type'         => Tinebase_Model_Tree_Node::TYPE_FILE,
+            'contenttype'  => $contentType,
+            'stream'       => $attachment,
+        ), true);
+        
+        $record->attachments->addRecord($node);
+        
+        $this->_event = Calendar_Controller_MSEventFacade::getInstance()->update($record);
+        $newAttachmentNode = $this->_event->attachments->filter('name', $name)->getFirstRecord();
+        
+        return $newAttachmentNode->object_id;
+    }
+    
+    /**
      * this function creates a Calendar_Model_Event and stores it in the database
      * 
      * @todo the header handling does not belong here. It should be moved to the DAV_Server class when supported
@@ -402,13 +434,23 @@ class Calendar_Frontend_WebDAV_Event extends Sabre\DAV\File implements Sabre\Cal
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . print_r($event->toArray(), true));
         
+        $this->update($event);
+        
+        return $this->getETag();
+    }
+    
+    /**
+     * update this node with given event
+     * 
+     * @param Calendar_Model_Event $event
+     */
+    public function update(Calendar_Model_Event $event)
+    {
         try {
             $this->_event = Calendar_Controller_MSEventFacade::getInstance()->update($event);
         } catch (Tinebase_Timemachine_Exception_ConcurrencyConflict $ttecc) {
             throw new Sabre\DAV\Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
         }
-        
-        return $this->getETag();
     }
     
     /**
@@ -479,6 +521,16 @@ class Calendar_Frontend_WebDAV_Event extends Sabre\DAV\File implements Sabre\Cal
     }
     
     /**
+     * returns container of this event
+     *
+     * @return Tinebase_Model_Container
+     */
+    public function getContainer()
+    {
+        return $this->_container;
+    }
+    
+    /**
      * return vcard and convert Calendar_Model_Event to vcard if needed
      * 
      * @return string
index a033724..a82b40c 100644 (file)
@@ -1197,7 +1197,7 @@ class Tinebase_FileSystem implements Tinebase_Controller_Interface
      * copy tempfile data to file path
      * 
      * @param  mixed   $tempFile
-         Tinebase_Model_Tree_Node     with property tempfile or stream
+         Tinebase_Model_Tree_Node     with property hash, tempfile or stream
          Tinebase_Model_Tempfile      tempfile
          string                       with tempFile id
          array                        with [id] => tempFile id (this is odd IMHO)
@@ -1222,7 +1222,10 @@ class Tinebase_FileSystem implements Tinebase_Controller_Interface
         }
         
         else if ($tempFile instanceof Tinebase_Model_Tree_Node) {
-            if (is_resource($tempFile->stream)) {
+            if (isset($tempFile->hash)) {
+                $hashFile = $this->_basePath . '/' . substr($tempFile->hash, 0, 3) . '/' . substr($tempFile->hash, 3);
+                $tempStream = fopen($hashFile, 'r');
+            } else if (is_resource($tempFile->stream)) {
                 $tempStream = $tempFile->stream;
             } else {
                 return $this->copyTempfile($tempFile->tempFile, $path);
index ed64dbf..2b8ff1d 100644 (file)
@@ -71,18 +71,26 @@ class Tinebase_Server_WebDAV extends Tinebase_Server_Abstract implements Tinebas
         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .' requestUri:' . $this->_request->getRequestUri());
         
+        self::$_server = new \Sabre\DAV\Server(new Tinebase_WebDav_Root());
+        
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
-            // NOTE inputstream can not be rewinded
-            $debugStream = fopen('php://temp','r+');
-            stream_copy_to_stream($this->_body, $debugStream);
-            rewind($debugStream);
-            $this->_body = $debugStream;
+            $contentType = self::$_server->httpRequest->getHeader('Content-Type');
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " requestContentType: " . $contentType);
             
-            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " <<< *DAV request\n" . stream_get_contents($this->_body));
-            rewind($this->_body);
+            if (preg_match('/^text/', $contentType)) {
+                // NOTE inputstream can not be rewinded
+                $debugStream = fopen('php://temp','r+');
+                stream_copy_to_stream($this->_body, $debugStream);
+                rewind($debugStream);
+                $this->_body = $debugStream;
+                
+                Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " <<< *DAV request\n" . stream_get_contents($this->_body));
+                rewind($this->_body);
+            } else {
+                Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " <<< *DAV request\n -- BINARY DATA --");
+            }
         }
         
-        self::$_server = new \Sabre\DAV\Server(new Tinebase_WebDav_Root());
         self::$_server->httpRequest->setBody($this->_body);
         
         // compute base uri
@@ -120,6 +128,7 @@ class Tinebase_Server_WebDAV extends Tinebase_Server_Abstract implements Tinebas
         self::$_server->addPlugin(new \Sabre\CalDAV\SharingPlugin());
         self::$_server->addPlugin(new Calendar_Frontend_CalDAV_PluginAutoSchedule());
         self::$_server->addPlugin(new Calendar_Frontend_CalDAV_PluginDefaultAlarms());
+        self::$_server->addPlugin(new Calendar_Frontend_CalDAV_PluginManagedAttachments());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_Inverse());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_OwnCloud());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_PrincipalSearch());