[Pkg-owncloud-commits] [php-sabredav] 01/275: Moved all scheduling related functionality.

David Prévot taffit at moszumanska.debian.org
Thu Sep 25 14:55:44 UTC 2014


This is an automated email from the git hooks/post-receive script.

taffit pushed a commit to branch master
in repository php-sabredav.

commit a807c586dbff04a84d322787e0546c80c8fa449d
Author: Evert Pot <evert at rooftopsolutions.nl>
Date:   Fri Aug 16 17:57:21 2013 +0100

    Moved all scheduling related functionality.
    
    This adds a new plugin Sabre\CalDAV\Schedule\Plugin, and is the first
    step towards calendar-auto-schedule support.
---
 lib/Sabre/CalDAV/Plugin.php                        | 483 -----------------
 lib/Sabre/CalDAV/Schedule/Plugin.php               | 598 +++++++++++++++++++++
 .../CalDAV/{ => Schedule}/FreeBusyRequestTest.php  |  21 +-
 .../Sabre/CalDAV/{ => Schedule}/OutboxPostTest.php |  31 +-
 tests/Sabre/DAVServerTest.php                      |  12 +
 tests/bootstrap.php                                |   1 +
 6 files changed, 639 insertions(+), 507 deletions(-)

diff --git a/lib/Sabre/CalDAV/Plugin.php b/lib/Sabre/CalDAV/Plugin.php
index 609ccd2..f5c905a 100644
--- a/lib/Sabre/CalDAV/Plugin.php
+++ b/lib/Sabre/CalDAV/Plugin.php
@@ -46,33 +46,6 @@ class Plugin extends DAV\ServerPlugin {
     protected $server;
 
     /**
-     * The email handler for invites and other scheduling messages.
-     *
-     * @var Schedule\IMip
-     */
-    protected $imipHandler;
-
-    /**
-     * Sets the iMIP handler.
-     *
-     * iMIP = The email transport of iCalendar scheduling messages. Setting
-     * this is optional, but if you want the server to allow invites to be sent
-     * out, you must set a handler.
-     *
-     * Specifically iCal will plain assume that the server supports this. If
-     * the server doesn't, iCal will display errors when inviting people to
-     * events.
-     *
-     * @param Schedule\IMip $imipHandler
-     * @return void
-     */
-    public function setIMipHandler(Schedule\IMip $imipHandler) {
-
-        $this->imipHandler = $imipHandler;
-
-    }
-
-    /**
      * Use this method to tell the server this plugin defines additional
      * HTTP methods.
      *
@@ -186,7 +159,6 @@ class Plugin extends DAV\ServerPlugin {
         $this->server = $server;
 
         $server->on('method:MKCALENDAR',   [$this,'httpMkcalendar']);
-        $server->on('method:POST',         [$this,'httpPost']);
         $server->on('method:GET',          [$this,'httpGet'], 90);
         $server->on('report',              [$this,'report']);
         $server->on('beforeGetProperties', [$this,'beforeGetProperties']);
@@ -238,38 +210,6 @@ class Plugin extends DAV\ServerPlugin {
     }
 
     /**
-     * This method handles POST request for the outbox.
-     *
-     * @param RequestInterface $request
-     * @param ResponseInterface $response
-     * @return bool
-     */
-    public function httpPost(RequestInterface $request, ResponseInterface $response) {
-
-        // Checking if this is a text/calendar content type
-        $contentType = $request->getHeader('Content-Type');
-        if (strpos($contentType, 'text/calendar')!==0) {
-            return;
-        }
-
-        $path = $request->getPath();
-
-        // Checking if we're talking to an outbox
-        try {
-            $node = $this->server->tree->getNodeForPath($path);
-        } catch (DAV\Exception\NotFound $e) {
-            return;
-        }
-        if (!$node instanceof Schedule\IOutbox)
-            return;
-
-        $this->server->transactionType = 'post-caldav-outbox';
-        $this->outboxRequest($node, $path);
-        return false;
-
-    }
-
-    /**
      * This functions handles REPORT requests specific to CalDAV
      *
      * @param string $reportName
@@ -910,431 +850,8 @@ class Plugin extends DAV\ServerPlugin {
 
     }
 
-    /**
-     * This method handles POST requests to the schedule-outbox.
-     *
-     * Currently, two types of requests are support:
-     *   * FREEBUSY requests from RFC 6638
-     *   * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
-     *
-     * The latter is from an expired early draft of the CalDAV scheduling
-     * extensions, but iCal depends on a feature from that spec, so we
-     * implement it.
-     *
-     * @param Schedule\IOutbox $outboxNode
-     * @param string $outboxUri
-     * @return void
-     */
-    public function outboxRequest(Schedule\IOutbox $outboxNode, $outboxUri) {
-
-        // Parsing the request body
-        try {
-            $vObject = VObject\Reader::read($this->server->httpRequest->getBody(true));
-        } catch (VObject\ParseException $e) {
-            throw new DAV\Exception\BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
-        }
-
-        // The incoming iCalendar object must have a METHOD property, and a
-        // component. The combination of both determines what type of request
-        // this is.
-        $componentType = null;
-        foreach($vObject->getComponents() as $component) {
-            if ($component->name !== 'VTIMEZONE') {
-                $componentType = $component->name;
-                break;
-            }
-        }
-        if (is_null($componentType)) {
-            throw new DAV\Exception\BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
-        }
-
-        // Validating the METHOD
-        $method = strtoupper((string)$vObject->METHOD);
-        if (!$method) {
-            throw new DAV\Exception\BadRequest('A METHOD property must be specified in iTIP messages');
-        }
-
-        // So we support two types of requests:
-        //
-        // REQUEST with a VFREEBUSY component
-        // REQUEST, REPLY, ADD, CANCEL on VEVENT components
-
-        $acl = $this->server->getPlugin('acl');
-
-        if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') {
-
-            $acl && $acl->checkPrivileges($outboxUri,'{' . Plugin::NS_CALDAV . '}schedule-query-freebusy');
-            $this->handleFreeBusyRequest($outboxNode, $vObject);
-
-        } elseif ($componentType === 'VEVENT' && in_array($method, array('REQUEST','REPLY','ADD','CANCEL'))) {
-
-            $acl && $acl->checkPrivileges($outboxUri,'{' . Plugin::NS_CALDAV . '}schedule-post-vevent');
-            $this->handleEventNotification($outboxNode, $vObject);
-
-        } else {
-
-            throw new DAV\Exception\NotImplemented('SabreDAV supports only VFREEBUSY (REQUEST) and VEVENT (REQUEST, REPLY, ADD, CANCEL)');
-
-        }
-
-    }
-
-    /**
-     * This method handles the REQUEST, REPLY, ADD and CANCEL methods for
-     * VEVENT iTip messages.
-     *
-     * @return void
-     */
-    protected function handleEventNotification(Schedule\IOutbox $outboxNode, VObject\Component $vObject) {
-
-        $originator = $this->server->httpRequest->getHeader('Originator');
-        $recipients = $this->server->httpRequest->getHeader('Recipient');
-
-        if (!$originator) {
-            throw new DAV\Exception\BadRequest('The Originator: header must be specified when making POST requests');
-        }
-        if (!$recipients) {
-            throw new DAV\Exception\BadRequest('The Recipient: header must be specified when making POST requests');
-        }
-
-        $recipients = explode(',',$recipients);
-        foreach($recipients as $k=>$recipient) {
-
-            $recipient = trim($recipient);
-            if (!preg_match('/^mailto:(.*)@(.*)$/i', $recipient)) {
-                throw new DAV\Exception\BadRequest('Recipients must start with mailto: and must be valid email address');
-            }
-            $recipient = substr($recipient, 7);
-            $recipients[$k] = $recipient;
-        }
-
-        // We need to make sure that 'originator' matches the currently
-        // authenticated user.
-        $aclPlugin = $this->server->getPlugin('acl');
-        if (is_null($aclPlugin)) throw new DAV\Exception('The ACL plugin must be loaded for scheduling to work');
-        $principal = $aclPlugin->getCurrentUserPrincipal();
-
-        $props = $this->server->getProperties($principal,array(
-            '{' . self::NS_CALDAV . '}calendar-user-address-set',
-        ));
-
-        $addresses = array();
-        if (isset($props['{' . self::NS_CALDAV . '}calendar-user-address-set'])) {
-            $addresses = $props['{' . self::NS_CALDAV . '}calendar-user-address-set']->getHrefs();
-        }
-
-        $found = false;
-        foreach($addresses as $address) {
-
-            // Trimming the / on both sides, just in case..
-            if (rtrim(strtolower($originator),'/') === rtrim(strtolower($address),'/')) {
-                $found = true;
-                break;
-            }
-
-        }
-
-        if (!$found) {
-            throw new DAV\Exception\Forbidden('The addresses specified in the Originator header did not match any addresses in the owners calendar-user-address-set header');
-        }
-
-        // If the Originator header was a url, and not a mailto: address..
-        // we're going to try to pull the mailto: from the vobject body.
-        if (strtolower(substr($originator,0,7)) !== 'mailto:') {
-            $originator = (string)$vObject->VEVENT->ORGANIZER;
-
-        }
-        if (strtolower(substr($originator,0,7)) !== 'mailto:') {
-            throw new DAV\Exception\Forbidden('Could not find mailto: address in both the Orignator header, and the ORGANIZER property in the VEVENT');
-        }
-        $originator = substr($originator,7);
-
-        $result = $this->iMIPMessage($originator, $recipients, $vObject, $principal);
-        $this->server->httpResponse->setStatus(200);
-        $this->server->httpResponse->setHeader('Content-Type','application/xml');
-        $this->server->httpResponse->setBody($this->generateScheduleResponse($result));
-
-    }
-
-    /**
-     * Sends an iMIP message by email.
-     *
-     * This method must return an array with status codes per recipient.
-     * This should look something like:
-     *
-     * array(
-     *    'user1 at example.org' => '2.0;Success'
-     * )
-     *
-     * Formatting for this status code can be found at:
-     * https://tools.ietf.org/html/rfc5545#section-3.8.8.3
-     *
-     * A list of valid status codes can be found at:
-     * https://tools.ietf.org/html/rfc5546#section-3.6
-     *
-     * @param string $originator
-     * @param array $recipients
-     * @param VObject\Component $vObject
-     * @param string $principal Principal url
-     * @return array
-     */
-    protected function iMIPMessage($originator, array $recipients, VObject\Component $vObject, $principal) {
-
-        if (!$this->imipHandler) {
-            $resultStatus = '5.2;This server does not support this operation';
-        } else {
-            $this->imipHandler->sendMessage($originator, $recipients, $vObject, $principal);
-            $resultStatus = '2.0;Success';
-        }
-
-        $result = array();
-        foreach($recipients as $recipient) {
-            $result[$recipient] = $resultStatus;
-        }
-
-        return $result;
-
-    }
-
-    /**
-     * Generates a schedule-response XML body
-     *
-     * The recipients array is a key->value list, containing email addresses
-     * and iTip status codes. See the iMIPMessage method for a description of
-     * the value.
-     *
-     * @param array $recipients
-     * @return string
-     */
-    public function generateScheduleResponse(array $recipients) {
-
-        $dom = new \DOMDocument('1.0','utf-8');
-        $dom->formatOutput = true;
-        $xscheduleResponse = $dom->createElement('cal:schedule-response');
-        $dom->appendChild($xscheduleResponse);
-
-        foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
-
-            $xscheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
-
-        }
-
-        foreach($recipients as $recipient=>$status) {
-            $xresponse = $dom->createElement('cal:response');
-
-            $xrecipient = $dom->createElement('cal:recipient');
-            $xrecipient->appendChild($dom->createTextNode($recipient));
-            $xresponse->appendChild($xrecipient);
-
-            $xrequestStatus = $dom->createElement('cal:request-status');
-            $xrequestStatus->appendChild($dom->createTextNode($status));
-            $xresponse->appendChild($xrequestStatus);
-
-            $xscheduleResponse->appendChild($xresponse);
-
-        }
-
-        return $dom->saveXML();
-
-    }
-
-    /**
-     * This method is responsible for parsing a free-busy query request and
-     * returning it's result.
-     *
-     * @param Schedule\IOutbox $outbox
-     * @param string $request
-     * @return string
-     */
-    protected function handleFreeBusyRequest(Schedule\IOutbox $outbox, VObject\Component $vObject) {
-
-        $vFreeBusy = $vObject->VFREEBUSY;
-        $organizer = $vFreeBusy->organizer;
-
-        $organizer = (string)$organizer;
-
-        // Validating if the organizer matches the owner of the inbox.
-        $owner = $outbox->getOwner();
-
-        $caldavNS = '{' . Plugin::NS_CALDAV . '}';
-
-        $uas = $caldavNS . 'calendar-user-address-set';
-        $props = $this->server->getProperties($owner,array($uas));
-
-        if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
-            throw new DAV\Exception\Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
-        }
-
-        if (!isset($vFreeBusy->ATTENDEE)) {
-            throw new DAV\Exception\BadRequest('You must at least specify 1 attendee');
-        }
-
-        $attendees = array();
-        foreach($vFreeBusy->ATTENDEE as $attendee) {
-            $attendees[]= (string)$attendee;
-        }
-
-
-        if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
-            throw new DAV\Exception\BadRequest('DTSTART and DTEND must both be specified');
-        }
-
-        $startRange = $vFreeBusy->DTSTART->getDateTime();
-        $endRange = $vFreeBusy->DTEND->getDateTime();
-
-        $results = array();
-        foreach($attendees as $attendee) {
-            $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
-        }
-
-        $dom = new \DOMDocument('1.0','utf-8');
-        $dom->formatOutput = true;
-        $scheduleResponse = $dom->createElement('cal:schedule-response');
-        foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
-
-            $scheduleResponse->setAttribute('xmlns:' . $prefix,$namespace);
-
-        }
-        $dom->appendChild($scheduleResponse);
-
-        foreach($results as $result) {
-            $response = $dom->createElement('cal:response');
-
-            $recipient = $dom->createElement('cal:recipient');
-            $recipientHref = $dom->createElement('d:href');
-
-            $recipientHref->appendChild($dom->createTextNode($result['href']));
-            $recipient->appendChild($recipientHref);
-            $response->appendChild($recipient);
-
-            $reqStatus = $dom->createElement('cal:request-status');
-            $reqStatus->appendChild($dom->createTextNode($result['request-status']));
-            $response->appendChild($reqStatus);
-
-            if (isset($result['calendar-data'])) {
-
-                $calendardata = $dom->createElement('cal:calendar-data');
-                $calendardata->appendChild($dom->createTextNode(str_replace("\r\n","\n",$result['calendar-data']->serialize())));
-                $response->appendChild($calendardata);
-
-            }
-            $scheduleResponse->appendChild($response);
-        }
-
-        $this->server->httpResponse->setStatus(200);
-        $this->server->httpResponse->setHeader('Content-Type','application/xml');
-        $this->server->httpResponse->setBody($dom->saveXML());
-
-    }
-
-    /**
-     * Returns free-busy information for a specific address. The returned
-     * data is an array containing the following properties:
-     *
-     * calendar-data : A VFREEBUSY VObject
-     * request-status : an iTip status code.
-     * href: The principal's email address, as requested
-     *
-     * The following request status codes may be returned:
-     *   * 2.0;description
-     *   * 3.7;description
-     *
-     * @param string $email address
-     * @param \DateTime $start
-     * @param \DateTime $end
-     * @param VObject\Component $request
-     * @return array
-     */
-    protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) {
-
-        $caldavNS = '{' . Plugin::NS_CALDAV . '}';
-
-        $aclPlugin = $this->server->getPlugin('acl');
-        if (substr($email,0,7)==='mailto:') $email = substr($email,7);
-
-        $result = $aclPlugin->principalSearch(
-            array('{http://sabredav.org/ns}email-address' => $email),
-            array(
-                '{DAV:}principal-URL', $caldavNS . 'calendar-home-set',
-                '{http://sabredav.org/ns}email-address',
-            )
-        );
-
-        if (!count($result)) {
-            return array(
-                'request-status' => '3.7;Could not find principal',
-                'href' => 'mailto:' . $email,
-            );
-        }
-
-        if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
-            return array(
-                'request-status' => '3.7;No calendar-home-set property found',
-                'href' => 'mailto:' . $email,
-            );
-        }
-        $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
-
-        // Grabbing the calendar list
-        $objects = array();
-        foreach($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
-            if (!$node instanceof ICalendar) {
-                continue;
-            }
-            $aclPlugin->checkPrivileges($homeSet . $node->getName() ,$caldavNS . 'read-free-busy');
-
-            // Getting the list of object uris within the time-range
-            $urls = $node->calendarQuery(array(
-                'name' => 'VCALENDAR',
-                'comp-filters' => array(
-                    array(
-                        'name' => 'VEVENT',
-                        'comp-filters' => array(),
-                        'prop-filters' => array(),
-                        'is-not-defined' => false,
-                        'time-range' => array(
-                            'start' => $start,
-                            'end' => $end,
-                        ),
-                    ),
-                ),
-                'prop-filters' => array(),
-                'is-not-defined' => false,
-                'time-range' => null,
-            ));
 
-            $calObjects = array_map(function($url) use ($node) {
-                $obj = $node->getChild($url)->get();
-                return $obj;
-            }, $urls);
 
-            $objects = array_merge($objects,$calObjects);
-
-        }
-
-        $vcalendar = new VObject\Component\VCalendar();
-        $vcalendar->VERSION = '2.0';
-        $vcalendar->METHOD = 'REPLY';
-        $vcalendar->CALSCALE = 'GREGORIAN';
-        $vcalendar->PRODID = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
-
-        $generator = new VObject\FreeBusyGenerator();
-        $generator->setObjects($objects);
-        $generator->setTimeRange($start, $end);
-        $generator->setBaseObject($vcalendar);
-
-        $result = $generator->getResult();
-
-        $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
-        $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID;
-        $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
-
-        return array(
-            'calendar-data' => $result,
-            'request-status' => '2.0;Success',
-            'href' => 'mailto:' . $email,
-        );
-    }
 
     /**
      * This method is used to generate HTML output for the
diff --git a/lib/Sabre/CalDAV/Schedule/Plugin.php b/lib/Sabre/CalDAV/Schedule/Plugin.php
new file mode 100644
index 0000000..8763f03
--- /dev/null
+++ b/lib/Sabre/CalDAV/Schedule/Plugin.php
@@ -0,0 +1,598 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use
+    Sabre\DAV\Server,
+    Sabre\DAV\ServerPlugin,
+    Sabre\HTTP\RequestInterface,
+    Sabre\HTTP\ResponseInterface,
+    Sabre\VObject,
+    Sabre\CalDAV\ICalendar,
+    Sabre\DAV\Exception\NotFound,
+    Sabre\DAV\Exception\Forbidden,
+    Sabre\DAV\Exception\BadRequest,
+    Sabre\DAV\Exception\NotImplemented;
+
+/**
+ * CalDAV scheduling plugin.
+ *
+ * This plugin provides the functionality added by the "Scheduling Extensions
+ * to CalDAV" standard, as defined in RFC6638.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Plugin extends ServerPlugin {
+
+    /**
+     * This is the official CalDAV namespace
+     */
+    const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
+
+    /**
+     * Reference to main Server object.
+     *
+     * @var Server
+     */
+    protected $server;
+
+    /**
+     * The email handler for invites and other scheduling messages.
+     *
+     * @var IMip
+     */
+    protected $imipHandler;
+
+    /**
+     * Sets the iMIP handler.
+     *
+     * iMIP = The email transport of iCalendar scheduling messages. Setting
+     * this is optional, but if you want the server to allow invites to be sent
+     * out, you must set a handler.
+     *
+     * Specifically iCal will plain assume that the server supports this. If
+     * the server doesn't, iCal will display errors when inviting people to
+     * events.
+     *
+     * @param IMip $imipHandler
+     * @return void
+     */
+    public function setIMipHandler(IMip $imipHandler) {
+
+        $this->imipHandler = $imipHandler;
+
+    }
+
+    /**
+     * Returns a list of features for the DAV: HTTP header.
+     *
+     * @return array
+     */
+    public function getFeatures() {
+
+        return ['calendar-auth-schedule'];
+
+    }
+
+    /**
+     * Returns the name of the plugin.
+     *
+     * Using this name other plugins will be able to access other plugins
+     * using Server::getPlugin
+     *
+     * @return string
+     */
+    public function getPluginName() {
+
+        return 'caldav-schedule';
+
+    }
+
+    /**
+     * Initializes the plugin
+     *
+     * @param Server $server
+     * @return void
+     */
+    public function initialize(Server $server) {
+
+        $this->server = $server;
+        $server->on('method:POST', [$this,'httpPost']);
+
+    }
+
+    /**
+     * Use this method to tell the server this plugin defines additional
+     * HTTP methods.
+     *
+     * This method is passed a uri. It should only return HTTP methods that are
+     * available for the specified uri.
+     *
+     * @param string $uri
+     * @return array
+     */
+    public function getHTTPMethods($uri) {
+
+        try {
+            $node = $this->server->tree->getNodeForPath($uri);
+        } catch (NotFound $e) {
+            return [];
+        }
+
+        if ($node instanceof IOutbox) {
+            return ['POST'];
+        }
+
+        return [];
+
+    }
+
+    /**
+     * This method handles POST request for the outbox.
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool
+     */
+    public function httpPost(RequestInterface $request, ResponseInterface $response) {
+
+        // Checking if this is a text/calendar content type
+        $contentType = $request->getHeader('Content-Type');
+        if (strpos($contentType, 'text/calendar')!==0) {
+            return;
+        }
+
+        $path = $request->getPath();
+
+        // Checking if we're talking to an outbox
+        try {
+            $node = $this->server->tree->getNodeForPath($path);
+        } catch (NotFound $e) {
+            return;
+        }
+        if (!$node instanceof IOutbox)
+            return;
+
+        $this->server->transactionType = 'post-caldav-outbox';
+        $this->outboxRequest($node, $request, $response);
+
+        // Returning false breaks the event chain and tells the server we've
+        // handled the request.
+        return false;
+
+    }
+
+    /**
+     * This method handles POST requests to the schedule-outbox.
+     *
+     * Currently, two types of requests are support:
+     *   * FREEBUSY requests from RFC 6638
+     *   * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
+     *
+     * The latter is from an expired early draft of the CalDAV scheduling
+     * extensions, but iCal depends on a feature from that spec, so we
+     * implement it.
+     *
+     * @param IOutbox $outboxNode
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return void
+     */
+    public function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response) {
+
+        $outboxPath = $request->getPath();
+
+        // Parsing the request body
+        try {
+            $vObject = VObject\Reader::read($request->getBody());
+        } catch (VObject\ParseException $e) {
+            throw new BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
+        }
+
+        // The incoming iCalendar object must have a METHOD property, and a
+        // component. The combination of both determines what type of request
+        // this is.
+        $componentType = null;
+        foreach($vObject->getComponents() as $component) {
+            if ($component->name !== 'VTIMEZONE') {
+                $componentType = $component->name;
+                break;
+            }
+        }
+        if (is_null($componentType)) {
+            throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
+        }
+
+        // Validating the METHOD
+        $method = strtoupper((string)$vObject->METHOD);
+        if (!$method) {
+            throw new BadRequest('A METHOD property must be specified in iTIP messages');
+        }
+
+        // So we support two types of requests:
+        //
+        // REQUEST with a VFREEBUSY component
+        // REQUEST, REPLY, ADD, CANCEL on VEVENT components
+
+        $acl = $this->server->getPlugin('acl');
+
+        if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') {
+
+            $acl && $acl->checkPrivileges($outboxPath, '{' . self::NS_CALDAV . '}schedule-query-freebusy');
+            $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response);
+
+        } elseif ($componentType === 'VEVENT' && in_array($method, ['REQUEST','REPLY','ADD','CANCEL'])) {
+
+            $acl && $acl->checkPrivileges($outboxPath, '{' . Plugin::NS_CALDAV . '}schedule-post-vevent');
+            $this->handleEventNotification($outboxNode, $vObject, $request, $response);
+
+        } else {
+
+            throw new NotImplemented('SabreDAV supports only VFREEBUSY (REQUEST) and VEVENT (REQUEST, REPLY, ADD, CANCEL)');
+
+        }
+
+    }
+
+    /**
+     * This method handles the REQUEST, REPLY, ADD and CANCEL methods for
+     * VEVENT iTip messages.
+     *
+     * @param IOutbox $outboxNode
+     * @param VObject\Component $vObject
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return void
+     */
+    protected function handleEventNotification(IOutbox $outboxNode, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response) {
+
+        $originator = $request->getHeader('Originator');
+        $recipients = $request->getHeader('Recipient');
+
+        if (!$originator) {
+            throw new BadRequest('The Originator: header must be specified when making POST requests');
+        }
+        if (!$recipients) {
+            throw new BadRequest('The Recipient: header must be specified when making POST requests');
+        }
+
+        $recipients = explode(',',$recipients);
+        foreach($recipients as $k=>$recipient) {
+
+            $recipient = trim($recipient);
+            if (!preg_match('/^mailto:(.*)@(.*)$/i', $recipient)) {
+                throw new BadRequest('Recipients must start with mailto: and must be valid email address');
+            }
+            $recipient = substr($recipient, 7);
+            $recipients[$k] = $recipient;
+        }
+
+        // We need to make sure that 'originator' matches the currently
+        // authenticated user.
+        $aclPlugin = $this->server->getPlugin('acl');
+        if (is_null($aclPlugin)) throw new DAV\Exception('The ACL plugin must be loaded for scheduling to work');
+        $principal = $aclPlugin->getCurrentUserPrincipal();
+
+        $props = $this->server->getProperties($principal, [
+            '{' . self::NS_CALDAV . '}calendar-user-address-set',
+        ]);
+
+        $addresses = [];
+        if (isset($props['{' . self::NS_CALDAV . '}calendar-user-address-set'])) {
+            $addresses = $props['{' . self::NS_CALDAV . '}calendar-user-address-set']->getHrefs();
+        }
+
+        $found = false;
+        foreach($addresses as $address) {
+
+            // Trimming the / on both sides, just in case..
+            if (rtrim(strtolower($originator),'/') === rtrim(strtolower($address),'/')) {
+                $found = true;
+                break;
+            }
+
+        }
+
+        if (!$found) {
+            throw new Forbidden('The addresses specified in the Originator header did not match any addresses in the owners calendar-user-address-set header');
+        }
+
+        // If the Originator header was a url, and not a mailto: address..
+        // we're going to try to pull the mailto: from the vobject body.
+        if (strtolower(substr($originator,0,7)) !== 'mailto:') {
+            $originator = (string)$vObject->VEVENT->ORGANIZER;
+
+        }
+        if (strtolower(substr($originator,0,7)) !== 'mailto:') {
+            throw new Forbidden('Could not find mailto: address in both the Orignator header, and the ORGANIZER property in the VEVENT');
+        }
+        $originator = substr($originator,7);
+
+        $result = $this->iMIPMessage($originator, $recipients, $vObject, $principal);
+        $response->setStatus(200);
+        $response->setHeader('Content-Type','application/xml');
+        $response->setBody($this->generateScheduleResponse($result));
+
+    }
+
+    /**
+     * Sends an iMIP message by email.
+     *
+     * This method must return an array with status codes per recipient.
+     * This should look something like:
+     *
+     * [
+     *    'user1 at example.org' => '2.0;Success'
+     * ]
+     *
+     * Formatting for this status code can be found at:
+     * https://tools.ietf.org/html/rfc5545#section-3.8.8.3
+     *
+     * A list of valid status codes can be found at:
+     * https://tools.ietf.org/html/rfc5546#section-3.6
+     *
+     * @param string $originator
+     * @param array $recipients
+     * @param VObject\Component $vObject
+     * @param string $principal Principal url
+     * @return array
+     */
+    protected function iMIPMessage($originator, array $recipients, VObject\Component $vObject, $principal) {
+
+        if (!$this->imipHandler) {
+            $resultStatus = '5.2;This server does not support this operation';
+        } else {
+            $this->imipHandler->sendMessage($originator, $recipients, $vObject, $principal);
+            $resultStatus = '2.0;Success';
+        }
+
+        $result = [];
+        foreach($recipients as $recipient) {
+            $result[$recipient] = $resultStatus;
+        }
+
+        return $result;
+
+    }
+
+    /**
+     * Generates a schedule-response XML body
+     *
+     * The recipients array is a key->value list, containing email addresses
+     * and iTip status codes. See the iMIPMessage method for a description of
+     * the value.
+     *
+     * @param array $recipients
+     * @return string
+     */
+    public function generateScheduleResponse(array $recipients) {
+
+        $dom = new \DOMDocument('1.0','utf-8');
+        $dom->formatOutput = true;
+        $xscheduleResponse = $dom->createElement('cal:schedule-response');
+        $dom->appendChild($xscheduleResponse);
+
+        foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
+
+            $xscheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
+
+        }
+
+        foreach($recipients as $recipient=>$status) {
+            $xresponse = $dom->createElement('cal:response');
+
+            $xrecipient = $dom->createElement('cal:recipient');
+            $xrecipient->appendChild($dom->createTextNode($recipient));
+            $xresponse->appendChild($xrecipient);
+
+            $xrequestStatus = $dom->createElement('cal:request-status');
+            $xrequestStatus->appendChild($dom->createTextNode($status));
+            $xresponse->appendChild($xrequestStatus);
+
+            $xscheduleResponse->appendChild($xresponse);
+
+        }
+
+        return $dom->saveXML();
+
+    }
+
+    /**
+     * This method is responsible for parsing a free-busy query request and
+     * returning it's result.
+     *
+     * @param IOutbox $outbox
+     * @param VObject\Component $vObject
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return string
+     */
+    protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response) {
+
+        $vFreeBusy = $vObject->VFREEBUSY;
+        $organizer = $vFreeBusy->organizer;
+
+        $organizer = (string)$organizer;
+
+        // Validating if the organizer matches the owner of the inbox.
+        $owner = $outbox->getOwner();
+
+        $caldavNS = '{' . self::NS_CALDAV . '}';
+
+        $uas = $caldavNS . 'calendar-user-address-set';
+        $props = $this->server->getProperties($owner, [$uas]);
+
+        if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
+            throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
+        }
+
+        if (!isset($vFreeBusy->ATTENDEE)) {
+            throw new BadRequest('You must at least specify 1 attendee');
+        }
+
+        $attendees = [];
+        foreach($vFreeBusy->ATTENDEE as $attendee) {
+            $attendees[]= (string)$attendee;
+        }
+
+
+        if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
+            throw new BadRequest('DTSTART and DTEND must both be specified');
+        }
+
+        $startRange = $vFreeBusy->DTSTART->getDateTime();
+        $endRange = $vFreeBusy->DTEND->getDateTime();
+
+        $results = [];
+        foreach($attendees as $attendee) {
+            $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
+        }
+
+        $dom = new \DOMDocument('1.0','utf-8');
+        $dom->formatOutput = true;
+        $scheduleResponse = $dom->createElement('cal:schedule-response');
+        foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
+
+            $scheduleResponse->setAttribute('xmlns:' . $prefix,$namespace);
+
+        }
+        $dom->appendChild($scheduleResponse);
+
+        foreach($results as $result) {
+            $xresponse = $dom->createElement('cal:response');
+
+            $recipient = $dom->createElement('cal:recipient');
+            $recipientHref = $dom->createElement('d:href');
+
+            $recipientHref->appendChild($dom->createTextNode($result['href']));
+            $recipient->appendChild($recipientHref);
+            $xresponse->appendChild($recipient);
+
+            $reqStatus = $dom->createElement('cal:request-status');
+            $reqStatus->appendChild($dom->createTextNode($result['request-status']));
+            $xresponse->appendChild($reqStatus);
+
+            if (isset($result['calendar-data'])) {
+
+                $calendardata = $dom->createElement('cal:calendar-data');
+                $calendardata->appendChild($dom->createTextNode(str_replace("\r\n","\n",$result['calendar-data']->serialize())));
+                $xresponse->appendChild($calendardata);
+
+            }
+            $scheduleResponse->appendChild($xresponse);
+        }
+
+        $response->setStatus(200);
+        $response->setHeader('Content-Type','application/xml');
+        $response->setBody($dom->saveXML());
+
+    }
+
+    /**
+     * Returns free-busy information for a specific address. The returned
+     * data is an array containing the following properties:
+     *
+     * calendar-data : A VFREEBUSY VObject
+     * request-status : an iTip status code.
+     * href: The principal's email address, as requested
+     *
+     * The following request status codes may be returned:
+     *   * 2.0;description
+     *   * 3.7;description
+     *
+     * @param string $email address
+     * @param \DateTime $start
+     * @param \DateTime $end
+     * @param VObject\Component $request
+     * @return array
+     */
+    protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) {
+
+        $caldavNS = '{' . Plugin::NS_CALDAV . '}';
+
+        $aclPlugin = $this->server->getPlugin('acl');
+        if (substr($email,0,7)==='mailto:') $email = substr($email,7);
+
+        $result = $aclPlugin->principalSearch(
+            ['{http://sabredav.org/ns}email-address' => $email],
+            [
+                '{DAV:}principal-URL', $caldavNS . 'calendar-home-set',
+                '{http://sabredav.org/ns}email-address',
+            ]
+        );
+
+        if (!count($result)) {
+            return [
+                'request-status' => '3.7;Could not find principal',
+                'href' => 'mailto:' . $email,
+            ];
+        }
+
+        if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
+            return [
+                'request-status' => '3.7;No calendar-home-set property found',
+                'href' => 'mailto:' . $email,
+            ];
+        }
+        $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
+
+        // Grabbing the calendar list
+        $objects = [];
+        foreach($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
+            if (!$node instanceof ICalendar) {
+                continue;
+            }
+            $aclPlugin->checkPrivileges($homeSet . $node->getName() ,$caldavNS . 'read-free-busy');
+
+            // Getting the list of object uris within the time-range
+            $urls = $node->calendarQuery([
+                'name' => 'VCALENDAR',
+                'comp-filters' => [
+                    [
+                        'name' => 'VEVENT',
+                        'comp-filters' => [],
+                        'prop-filters' => [],
+                        'is-not-defined' => false,
+                        'time-range' => [
+                            'start' => $start,
+                            'end' => $end,
+                        ],
+                    ],
+                ],
+                'prop-filters' => [],
+                'is-not-defined' => false,
+                'time-range' => null,
+            ]);
+
+            $calObjects = array_map(function($url) use ($node) {
+                $obj = $node->getChild($url)->get();
+                return $obj;
+            }, $urls);
+
+            $objects = array_merge($objects,$calObjects);
+
+        }
+
+        $vcalendar = new VObject\Component\VCalendar();
+        $vcalendar->METHOD = 'REPLY';
+
+        $generator = new VObject\FreeBusyGenerator();
+        $generator->setObjects($objects);
+        $generator->setTimeRange($start, $end);
+        $generator->setBaseObject($vcalendar);
+
+        $result = $generator->getResult();
+
+        $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
+        $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID;
+        $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
+
+        return [
+            'calendar-data' => $result,
+            'request-status' => '2.0;Success',
+            'href' => 'mailto:' . $email,
+        ];
+    }
+}
diff --git a/tests/Sabre/CalDAV/FreeBusyRequestTest.php b/tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php
similarity index 95%
rename from tests/Sabre/CalDAV/FreeBusyRequestTest.php
rename to tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php
index 327bc76..162705d 100644
--- a/tests/Sabre/CalDAV/FreeBusyRequestTest.php
+++ b/tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php
@@ -1,12 +1,12 @@
 <?php
 
-namespace Sabre\CalDAV;
+namespace Sabre\CalDAV\Schedule;
 
-use Sabre\DAV;
-use Sabre\DAVACL;
-use Sabre\HTTP;
-
-require_once 'Sabre/HTTP/ResponseMock.php';
+use
+    Sabre\DAV,
+    Sabre\DAVACL,
+    Sabre\HTTP,
+    Sabre\CalDAV;
 
 class FreeBusyRequestTest extends \PHPUnit_Framework_TestCase {
 
@@ -40,11 +40,11 @@ END:VCALENDAR',
         );
 
         $principalBackend = new DAVACL\PrincipalBackend\Mock();
-        $caldavBackend = new Backend\Mock($calendars, $calendarobjects);
+        $caldavBackend = new CalDAV\Backend\Mock($calendars, $calendarobjects);
 
         $tree = array(
             new DAVACL\PrincipalCollection($principalBackend),
-            new CalendarRootNode($principalBackend, $caldavBackend),
+            new CalDAV\CalendarRootNode($principalBackend, $caldavBackend),
         );
 
         $this->request = HTTP\Request::createFromServerArray([
@@ -64,6 +64,11 @@ END:VCALENDAR',
         $this->authPlugin = new DAV\Auth\Plugin($authBackend,'SabreDAV');
         $this->server->addPlugin($this->authPlugin);
 
+        // CalDAV plugin
+        $this->plugin = new CalDAV\Plugin();
+        $this->server->addPlugin($this->plugin);
+
+        // Scheduling plugin
         $this->plugin = new Plugin();
         $this->server->addPlugin($this->plugin);
 
diff --git a/tests/Sabre/CalDAV/OutboxPostTest.php b/tests/Sabre/CalDAV/Schedule/OutboxPostTest.php
similarity index 95%
rename from tests/Sabre/CalDAV/OutboxPostTest.php
rename to tests/Sabre/CalDAV/Schedule/OutboxPostTest.php
index 80a470c..97c8b90 100644
--- a/tests/Sabre/CalDAV/OutboxPostTest.php
+++ b/tests/Sabre/CalDAV/Schedule/OutboxPostTest.php
@@ -1,18 +1,17 @@
 <?php
 
-namespace Sabre\CalDAV;
+namespace Sabre\CalDAV\Schedule;
+
 use Sabre\HTTP;
 use Sabre\VObject;
 use Sabre\DAV;
 
-require_once 'Sabre/DAVServerTest.php';
-require_once 'Sabre/CalDAV/Schedule/IMip/Mock.php';
-
 class OutboxPostTest extends \Sabre\DAVServerTest {
 
     protected $setupCalDAV = true;
     protected $setupACL = true;
     protected $autoLogin = 'user1';
+    protected $setupCalDAVScheduling = true;
 
     function testPostPassThruNotFound() {
 
@@ -318,9 +317,9 @@ class OutboxPostTest extends \Sabre\DAVServerTest {
 
         $req->setBody(implode("\r\n",$body));
 
-        $handler = new Schedule\IMip\Mock('server at example.org');
+        $handler = new IMip\Mock('server at example.org');
 
-        $this->caldavPlugin->setIMIPhandler($handler);
+        $this->caldavSchedulePlugin->setIMIPhandler($handler);
 
         $response = $this->request($req);
         $this->assertEquals('200 OK', $response->status);
@@ -370,9 +369,9 @@ class OutboxPostTest extends \Sabre\DAVServerTest {
 
         $req->setBody(implode("\r\n",$body));
 
-        $handler = new Schedule\IMip\Mock('server at example.org');
+        $handler = new IMip\Mock('server at example.org');
 
-        $this->caldavPlugin->setIMIPhandler($handler);
+        $this->caldavSchedulePlugin->setIMIPhandler($handler);
 
         $response = $this->request($req);
         $this->assertEquals('200 OK', $response->status, 'Full body: ' . $response->body);
@@ -421,9 +420,9 @@ class OutboxPostTest extends \Sabre\DAVServerTest {
 
         $req->setBody(implode("\r\n",$body));
 
-        $handler = new Schedule\IMip\Mock('server at example.org');
+        $handler = new IMip\Mock('server at example.org');
 
-        $this->caldavPlugin->setIMIPhandler($handler);
+        $this->caldavSchedulePlugin->setIMIPhandler($handler);
 
         $response = $this->request($req);
         $this->assertEquals('200 OK', $response->status);
@@ -472,9 +471,9 @@ class OutboxPostTest extends \Sabre\DAVServerTest {
 
         $req->setBody(implode("\r\n",$body));
 
-        $handler = new Schedule\IMip\Mock('server at example.org');
+        $handler = new IMip\Mock('server at example.org');
 
-        $this->caldavPlugin->setIMIPhandler($handler);
+        $this->caldavSchedulePlugin->setIMIPhandler($handler);
         $this->assertHTTPStatus(200, $req);
 
         $this->assertEquals(array(
@@ -514,9 +513,9 @@ class OutboxPostTest extends \Sabre\DAVServerTest {
 
         $req->setBody(implode("\r\n",$body));
 
-        $handler = new Schedule\IMip\Mock('server at example.org');
+        $handler = new IMip\Mock('server at example.org');
 
-        $this->caldavPlugin->setIMIPhandler($handler);
+        $this->caldavSchedulePlugin->setIMIPhandler($handler);
         $this->assertHTTPStatus(200, $req);
 
         $this->assertEquals(array(
@@ -558,9 +557,9 @@ class OutboxPostTest extends \Sabre\DAVServerTest {
 
         $req->setBody(implode("\r\n",$body));
 
-        $handler = new Schedule\IMip\Mock('server at example.org');
+        $handler = new IMip\Mock('server at example.org');
 
-        $this->caldavPlugin->setIMIPhandler($handler);
+        $this->caldavSchedulePlugin->setIMIPhandler($handler);
 
         $response = $this->request($req);
         $this->assertEquals('403 Forbidden', $response->status, 'Full body: ' . $response->body);
diff --git a/tests/Sabre/DAVServerTest.php b/tests/Sabre/DAVServerTest.php
index 6927cc7..ae61a74 100644
--- a/tests/Sabre/DAVServerTest.php
+++ b/tests/Sabre/DAVServerTest.php
@@ -30,6 +30,7 @@ abstract class DAVServerTest extends \PHPUnit_Framework_TestCase {
     protected $setupCardDAV = false;
     protected $setupACL = false;
     protected $setupCalDAVSharing = false;
+    protected $setupCalDAVScheduling = false;
 
     protected $caldavCalendars = array();
     protected $caldavCalendarObjects = array();
@@ -68,6 +69,13 @@ abstract class DAVServerTest extends \PHPUnit_Framework_TestCase {
     protected $caldavSharingPlugin;
 
     /**
+     * CalDAV scheduling plugin
+     *
+     * @var CalDAV\Schedule\Plugin
+     */
+    protected $caldavSchedulePlugin;
+
+    /**
      * @var Sabre\DAV\Auth\Plugin
      */
     protected $authPlugin;
@@ -94,6 +102,10 @@ abstract class DAVServerTest extends \PHPUnit_Framework_TestCase {
             $this->caldavSharingPlugin = new CalDAV\SharingPlugin();
             $this->server->addPlugin($this->caldavSharingPlugin);
         }
+        if ($this->setupCalDAVScheduling) {
+            $this->caldavSchedulePlugin = new CalDAV\Schedule\Plugin();
+            $this->server->addPlugin($this->caldavSchedulePlugin);
+        }
         if ($this->setupCardDAV) {
             $this->carddavPlugin = new CardDAV\Plugin();
             $this->server->addPlugin($this->carddavPlugin);
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index d245715..919628c 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -8,6 +8,7 @@ include 'Sabre/DAVServerTest.php';
 include 'Sabre/CardDAV/Backend/AbstractPDOTest.php';
 include 'Sabre/CardDAV/TestUtil.php';
 include 'Sabre/DAV/ClientMock.php';
+include 'Sabre/CalDAV/Schedule/IMip/Mock.php';
 
 date_default_timezone_set('GMT');
 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-owncloud/php-sabredav.git



More information about the Pkg-owncloud-commits mailing list