aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/sabre/dav/lib/CalDAV/Schedule
diff options
context:
space:
mode:
authorredmatrix <git@macgirvin.com>2016-05-10 17:26:44 -0700
committerredmatrix <git@macgirvin.com>2016-05-10 17:26:44 -0700
commit0b02a6d123b2014705998c94ddf3d460948d3eac (patch)
tree78ff2cab9944a4f5ab3f80ec93cbe1120de90bb2 /vendor/sabre/dav/lib/CalDAV/Schedule
parent40b5b6e9d2da7ab65c8b4d38cdceac83a4d78deb (diff)
downloadvolse-hubzilla-0b02a6d123b2014705998c94ddf3d460948d3eac.tar.gz
volse-hubzilla-0b02a6d123b2014705998c94ddf3d460948d3eac.tar.bz2
volse-hubzilla-0b02a6d123b2014705998c94ddf3d460948d3eac.zip
initial sabre upgrade (needs lots of work - to wit: authentication, redo the browser interface, and rework event export/import)
Diffstat (limited to 'vendor/sabre/dav/lib/CalDAV/Schedule')
-rw-r--r--vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php15
-rw-r--r--vendor/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php190
-rw-r--r--vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php15
-rw-r--r--vendor/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php13
-rw-r--r--vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php261
-rw-r--r--vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php184
-rw-r--r--vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php994
-rw-r--r--vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php165
8 files changed, 1837 insertions, 0 deletions
diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php b/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php
new file mode 100644
index 000000000..c9fd77d93
--- /dev/null
+++ b/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+/**
+ * Implement this interface to have a node be recognized as a CalDAV scheduling
+ * inbox.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IInbox extends \Sabre\CalDAV\ICalendarObjectContainer, \Sabre\DAVACL\IACL {
+
+}
diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php b/vendor/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php
new file mode 100644
index 000000000..ffb1fe45b
--- /dev/null
+++ b/vendor/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use Sabre\DAV;
+use Sabre\VObject\ITip;
+
+/**
+ * iMIP handler.
+ *
+ * This class is responsible for sending out iMIP messages. iMIP is the
+ * email-based transport for iTIP. iTIP deals with scheduling operations for
+ * iCalendar objects.
+ *
+ * If you want to customize the email that gets sent out, you can do so by
+ * extending this class and overriding the sendMessage method.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class IMipPlugin extends DAV\ServerPlugin {
+
+ /**
+ * Email address used in From: header.
+ *
+ * @var string
+ */
+ protected $senderEmail;
+
+ /**
+ * ITipMessage
+ *
+ * @var ITip\Message
+ */
+ protected $itipMessage;
+
+ /**
+ * Creates the email handler.
+ *
+ * @param string $senderEmail. The 'senderEmail' is the email that shows up
+ * in the 'From:' address. This should
+ * generally be some kind of no-reply email
+ * address you own.
+ */
+ function __construct($senderEmail) {
+
+ $this->senderEmail = $senderEmail;
+
+ }
+
+ /*
+ * This initializes the plugin.
+ *
+ * This function is called by Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ function initialize(DAV\Server $server) {
+
+ $server->on('schedule', [$this, 'schedule'], 120);
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using \Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ function getPluginName() {
+
+ return 'imip';
+
+ }
+
+ /**
+ * Event handler for the 'schedule' event.
+ *
+ * @param ITip\Message $iTipMessage
+ * @return void
+ */
+ function schedule(ITip\Message $iTipMessage) {
+
+ // Not sending any emails if the system considers the update
+ // insignificant.
+ if (!$iTipMessage->significantChange) {
+ if (!$iTipMessage->scheduleStatus) {
+ $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
+ }
+ return;
+ }
+
+ $summary = $iTipMessage->message->VEVENT->SUMMARY;
+
+ if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto')
+ return;
+
+ if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto')
+ return;
+
+ $sender = substr($iTipMessage->sender, 7);
+ $recipient = substr($iTipMessage->recipient, 7);
+
+ if ($iTipMessage->senderName) {
+ $sender = $iTipMessage->senderName . ' <' . $sender . '>';
+ }
+ if ($iTipMessage->recipientName) {
+ $recipient = $iTipMessage->recipientName . ' <' . $recipient . '>';
+ }
+
+ $subject = 'SabreDAV iTIP message';
+ switch (strtoupper($iTipMessage->method)) {
+ case 'REPLY' :
+ $subject = 'Re: ' . $summary;
+ break;
+ case 'REQUEST' :
+ $subject = $summary;
+ break;
+ case 'CANCEL' :
+ $subject = 'Cancelled: ' . $summary;
+ break;
+ }
+
+ $headers = [
+ 'Reply-To: ' . $sender,
+ 'From: ' . $this->senderEmail,
+ 'Content-Type: text/calendar; charset=UTF-8; method=' . $iTipMessage->method,
+ ];
+ if (DAV\Server::$exposeVersion) {
+ $headers[] = 'X-Sabre-Version: ' . DAV\Version::VERSION;
+ }
+ $this->mail(
+ $recipient,
+ $subject,
+ $iTipMessage->message->serialize(),
+ $headers
+ );
+ $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
+
+ }
+
+ // @codeCoverageIgnoreStart
+ // This is deemed untestable in a reasonable manner
+
+ /**
+ * This function is responsible for sending the actual email.
+ *
+ * @param string $to Recipient email address
+ * @param string $subject Subject of the email
+ * @param string $body iCalendar body
+ * @param array $headers List of headers
+ * @return void
+ */
+ protected function mail($to, $subject, $body, array $headers) {
+
+ mail($to, $subject, $body, implode("\r\n", $headers));
+
+ }
+
+ // @codeCoverageIgnoreEnd
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ function getPluginInfo() {
+
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Email delivery (rfc6037) for CalDAV scheduling',
+ 'link' => 'http://sabre.io/dav/scheduling/',
+ ];
+
+ }
+
+}
diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php b/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php
new file mode 100644
index 000000000..88fbdc411
--- /dev/null
+++ b/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+/**
+ * Implement this interface to have a node be recognized as a CalDAV scheduling
+ * outbox.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IOutbox extends \Sabre\DAV\ICollection, \Sabre\DAVACL\IACL {
+
+}
diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php b/vendor/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php
new file mode 100644
index 000000000..b37cb40a1
--- /dev/null
+++ b/vendor/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+/**
+ * The SchedulingObject represents a scheduling object in the Inbox collection
+ *
+ * @license http://sabre.io/license/ Modified BSD License
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ */
+interface ISchedulingObject extends \Sabre\CalDAV\ICalendarObject {
+
+}
diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php b/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php
new file mode 100644
index 000000000..13212565e
--- /dev/null
+++ b/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php
@@ -0,0 +1,261 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use Sabre\DAV;
+use Sabre\CalDAV;
+use Sabre\DAVACL;
+use Sabre\CalDAV\Backend;
+use Sabre\VObject;
+
+/**
+ * The CalDAV scheduling inbox
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Inbox extends DAV\Collection implements IInbox {
+
+ /**
+ * CalDAV backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $caldavBackend;
+
+ /**
+ * The principal Uri
+ *
+ * @var string
+ */
+ protected $principalUri;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\SchedulingSupport $caldavBackend
+ * @param string $principalUri
+ */
+ function __construct(Backend\SchedulingSupport $caldavBackend, $principalUri) {
+
+ $this->caldavBackend = $caldavBackend;
+ $this->principalUri = $principalUri;
+
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ function getName() {
+
+ return 'inbox';
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return \Sabre\DAV\INode[]
+ */
+ function getChildren() {
+
+ $objs = $this->caldavBackend->getSchedulingObjects($this->principalUri);
+ $children = [];
+ foreach ($objs as $obj) {
+ //$obj['acl'] = $this->getACL();
+ $obj['principaluri'] = $this->principalUri;
+ $children[] = new SchedulingObject($this->caldavBackend, $obj);
+ }
+ return $children;
+
+ }
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After succesful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ function createFile($name, $data = null) {
+
+ $this->caldavBackend->createSchedulingObject($this->principalUri, $name, $data);
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ function getOwner() {
+
+ return $this->principalUri;
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ function getACL() {
+
+ return [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}unbind',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}unbind',
+ 'principal' => $this->getOwner() . '/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver-invite',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver-reply',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ],
+ ];
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ function getSupportedPrivilegeSet() {
+
+ $ns = '{' . CalDAV\Plugin::NS_CALDAV . '}';
+
+ $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
+ $default['aggregates'][] = [
+ 'privilege' => $ns . 'schedule-deliver',
+ 'aggregates' => [
+ ['privilege' => $ns . 'schedule-deliver-invite'],
+ ['privilege' => $ns . 'schedule-deliver-reply'],
+ ],
+ ];
+ return $default;
+
+ }
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by \Sabre\CalDAV\CalendarQueryParser.
+ *
+ * @param array $filters
+ * @return array
+ */
+ function calendarQuery(array $filters) {
+
+ $result = [];
+ $validator = new CalDAV\CalendarQueryValidator();
+
+ $objects = $this->caldavBackend->getSchedulingObjects($this->principalUri);
+ foreach ($objects as $object) {
+ $vObject = VObject\Reader::read($object['calendardata']);
+ if ($validator->validate($vObject, $filters)) {
+ $result[] = $object['uri'];
+ }
+
+ // Destroy circular references to PHP will GC the object.
+ $vObject->destroy();
+ }
+ return $result;
+
+ }
+
+}
diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php b/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php
new file mode 100644
index 000000000..dabaee2ca
--- /dev/null
+++ b/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use Sabre\DAV;
+use Sabre\CalDAV;
+use Sabre\DAVACL;
+
+/**
+ * The CalDAV scheduling outbox
+ *
+ * The outbox is mainly used as an endpoint in the tree for a client to do
+ * free-busy requests. This functionality is completely handled by the
+ * Scheduling plugin, so this object is actually mostly static.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Outbox extends DAV\Collection implements IOutbox {
+
+ /**
+ * The principal Uri
+ *
+ * @var string
+ */
+ protected $principalUri;
+
+ /**
+ * Constructor
+ *
+ * @param string $principalUri
+ */
+ function __construct($principalUri) {
+
+ $this->principalUri = $principalUri;
+
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ function getName() {
+
+ return 'outbox';
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return \Sabre\DAV\INode[]
+ */
+ function getChildren() {
+
+ return [];
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ function getOwner() {
+
+ return $this->principalUri;
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ function getACL() {
+
+ return [
+ [
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
+ 'principal' => $this->getOwner() . '/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
+ 'principal' => $this->getOwner() . '/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner() . '/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner() . '/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ ];
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ function getSupportedPrivilegeSet() {
+
+ $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
+ $default['aggregates'][] = [
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
+ ];
+ $default['aggregates'][] = [
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
+ ];
+
+ return $default;
+
+ }
+
+}
diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php b/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php
new file mode 100644
index 000000000..827d6209b
--- /dev/null
+++ b/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php
@@ -0,0 +1,994 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use DateTimeZone;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\PropPatch;
+use Sabre\DAV\INode;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\VObject;
+use Sabre\VObject\Reader;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\ITip;
+use Sabre\VObject\ITip\Message;
+use Sabre\DAVACL;
+use Sabre\CalDAV\ICalendar;
+use Sabre\CalDAV\ICalendarObject;
+use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\Exception\NotImplemented;
+
+/**
+ * CalDAV scheduling plugin.
+ * =========================
+ *
+ * This plugin provides the functionality added by the "Scheduling Extensions
+ * to CalDAV" standard, as defined in RFC6638.
+ *
+ * calendar-auto-schedule largely works by intercepting a users request to
+ * update their local calendar. If a user creates a new event with attendees,
+ * this plugin is supposed to grab the information from that event, and notify
+ * the attendees of this.
+ *
+ * There's 3 possible transports for this:
+ * * local delivery
+ * * delivery through email (iMip)
+ * * server-to-server delivery (iSchedule)
+ *
+ * iMip is simply, because we just need to add the iTip message as an email
+ * attachment. Local delivery is harder, because we both need to add this same
+ * message to a local DAV inbox, as well as live-update the relevant events.
+ *
+ * iSchedule is something for later.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/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;
+
+ /**
+ * Returns a list of features for the DAV: HTTP header.
+ *
+ * @return array
+ */
+ function getFeatures() {
+
+ return ['calendar-auto-schedule', 'calendar-availability'];
+
+ }
+
+ /**
+ * Returns the name of the plugin.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using Server::getPlugin
+ *
+ * @return string
+ */
+ function getPluginName() {
+
+ return 'caldav-schedule';
+
+ }
+
+ /**
+ * Initializes the plugin
+ *
+ * @param Server $server
+ * @return void
+ */
+ function initialize(Server $server) {
+
+ $this->server = $server;
+ $server->on('method:POST', [$this, 'httpPost']);
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('propPatch', [$this, 'propPatch']);
+ $server->on('calendarObjectChange', [$this, 'calendarObjectChange']);
+ $server->on('beforeUnbind', [$this, 'beforeUnbind']);
+ $server->on('schedule', [$this, 'scheduleLocalDelivery']);
+
+ $ns = '{' . self::NS_CALDAV . '}';
+
+ /**
+ * This information ensures that the {DAV:}resourcetype property has
+ * the correct values.
+ */
+ $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns . 'schedule-outbox';
+ $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns . 'schedule-inbox';
+
+ /**
+ * Properties we protect are made read-only by the server.
+ */
+ array_push($server->protectedProperties,
+ $ns . 'schedule-inbox-URL',
+ $ns . 'schedule-outbox-URL',
+ $ns . 'calendar-user-address-set',
+ $ns . 'calendar-user-type',
+ $ns . 'schedule-default-calendar-URL'
+ );
+
+ }
+
+ /**
+ * 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
+ */
+ 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
+ */
+ 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 handler is invoked during fetching of properties.
+ *
+ * We use this event to add calendar-auto-schedule-specific properties.
+ *
+ * @param PropFind $propFind
+ * @param INode $node
+ * @return void
+ */
+ function propFind(PropFind $propFind, INode $node) {
+
+ if ($node instanceof DAVACL\IPrincipal) {
+
+ $caldavPlugin = $this->server->getPlugin('caldav');
+ $principalUrl = $node->getPrincipalUrl();
+
+ // schedule-outbox-URL property
+ $propFind->handle('{' . self::NS_CALDAV . '}schedule-outbox-URL', function() use ($principalUrl, $caldavPlugin) {
+
+ $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
+ if (!$calendarHomePath) {
+ return null;
+ }
+ $outboxPath = $calendarHomePath . '/outbox/';
+
+ return new Href($outboxPath);
+
+ });
+ // schedule-inbox-URL property
+ $propFind->handle('{' . self::NS_CALDAV . '}schedule-inbox-URL', function() use ($principalUrl, $caldavPlugin) {
+
+ $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
+ if (!$calendarHomePath) {
+ return null;
+ }
+ $inboxPath = $calendarHomePath . '/inbox/';
+
+ return new Href($inboxPath);
+
+ });
+
+ $propFind->handle('{' . self::NS_CALDAV . '}schedule-default-calendar-URL', function() use ($principalUrl, $caldavPlugin) {
+
+ // We don't support customizing this property yet, so in the
+ // meantime we just grab the first calendar in the home-set.
+ $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
+
+ if (!$calendarHomePath) {
+ return null;
+ }
+
+ $sccs = '{' . self::NS_CALDAV . '}supported-calendar-component-set';
+
+ $result = $this->server->getPropertiesForPath($calendarHomePath, [
+ '{DAV:}resourcetype',
+ $sccs,
+ ], 1);
+
+ foreach ($result as $child) {
+ if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{' . self::NS_CALDAV . '}calendar') || $child[200]['{DAV:}resourcetype']->is('{http://calendarserver.org/ns/}shared')) {
+ // Node is either not a calendar or a shared instance.
+ continue;
+ }
+ if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) {
+ // Either there is no supported-calendar-component-set
+ // (which is fine) or we found one that supports VEVENT.
+ return new Href($child['href']);
+ }
+ }
+
+ });
+
+ // The server currently reports every principal to be of type
+ // 'INDIVIDUAL'
+ $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-type', function() {
+
+ return 'INDIVIDUAL';
+
+ });
+
+ }
+
+ // Mapping the old property to the new property.
+ $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function() use ($propFind, $node) {
+
+ // In case it wasn't clear, the only difference is that we map the
+ // old property to a different namespace.
+ $availProp = '{' . self::NS_CALDAV . '}calendar-availability';
+ $subPropFind = new PropFind(
+ $propFind->getPath(),
+ [$availProp]
+ );
+
+ $this->server->getPropertiesByNode(
+ $subPropFind,
+ $node
+ );
+
+ $propFind->set(
+ '{http://calendarserver.org/ns/}calendar-availability',
+ $subPropFind->get($availProp),
+ $subPropFind->getStatus($availProp)
+ );
+
+ });
+
+ }
+
+ /**
+ * This method is called during property updates.
+ *
+ * @param string $path
+ * @param PropPatch $propPatch
+ * @return void
+ */
+ function propPatch($path, PropPatch $propPatch) {
+
+ // Mapping the old property to the new property.
+ $propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function($value) use ($path) {
+
+ $availProp = '{' . self::NS_CALDAV . '}calendar-availability';
+ $subPropPatch = new PropPatch([$availProp => $value]);
+ $this->server->emit('propPatch', [$path, $subPropPatch]);
+ $subPropPatch->commit();
+
+ return $subPropPatch->getResult()[$availProp];
+
+ });
+
+ }
+
+ /**
+ * This method is triggered whenever there was a calendar object gets
+ * created or updated.
+ *
+ * @param RequestInterface $request HTTP request
+ * @param ResponseInterface $response HTTP Response
+ * @param VCalendar $vCal Parsed iCalendar object
+ * @param mixed $calendarPath Path to calendar collection
+ * @param mixed $modified The iCalendar object has been touched.
+ * @param mixed $isNew Whether this was a new item or we're updating one
+ * @return void
+ */
+ function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) {
+
+ if (!$this->scheduleReply($this->server->httpRequest)) {
+ return;
+ }
+
+ $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
+
+ $addresses = $this->getAddressesForPrincipal(
+ $calendarNode->getOwner()
+ );
+
+ if (!$isNew) {
+ $node = $this->server->tree->getNodeForPath($request->getPath());
+ $oldObj = Reader::read($node->get());
+ } else {
+ $oldObj = null;
+ }
+
+ $this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified);
+
+ if ($oldObj) {
+ // Destroy circular references so PHP will GC the object.
+ $oldObj->destroy();
+ }
+
+ }
+
+ /**
+ * This method is responsible for delivering the ITip message.
+ *
+ * @param ITip\Message $itipMessage
+ * @return void
+ */
+ function deliver(ITip\Message $iTipMessage) {
+
+ $this->server->emit('schedule', [$iTipMessage]);
+ if (!$iTipMessage->scheduleStatus) {
+ $iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message';
+ }
+ // In case the change was considered 'insignificant', we are going to
+ // remove any error statuses, if any. See ticket #525.
+ list($baseCode) = explode('.', $iTipMessage->scheduleStatus);
+ if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) {
+ $iTipMessage->scheduleStatus = null;
+ }
+
+ }
+
+ /**
+ * This method is triggered before a file gets deleted.
+ *
+ * We use this event to make sure that when this happens, attendees get
+ * cancellations, and organizers get 'DECLINED' statuses.
+ *
+ * @param string $path
+ * @return void
+ */
+ function beforeUnbind($path) {
+
+ // FIXME: We shouldn't trigger this functionality when we're issuing a
+ // MOVE. This is a hack.
+ if ($this->server->httpRequest->getMethod() === 'MOVE') return;
+
+ $node = $this->server->tree->getNodeForPath($path);
+
+ if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
+ return;
+ }
+
+ if (!$this->scheduleReply($this->server->httpRequest)) {
+ return;
+ }
+
+ $addresses = $this->getAddressesForPrincipal(
+ $node->getOwner()
+ );
+
+ $broker = new ITip\Broker();
+ $messages = $broker->parseEvent(null, $addresses, $node->get());
+
+ foreach ($messages as $message) {
+ $this->deliver($message);
+ }
+
+ }
+
+ /**
+ * Event handler for the 'schedule' event.
+ *
+ * This handler attempts to look at local accounts to deliver the
+ * scheduling object.
+ *
+ * @param ITip\Message $iTipMessage
+ * @return void
+ */
+ function scheduleLocalDelivery(ITip\Message $iTipMessage) {
+
+ $aclPlugin = $this->server->getPlugin('acl');
+
+ // Local delivery is not available if the ACL plugin is not loaded.
+ if (!$aclPlugin) {
+ return;
+ }
+
+ $caldavNS = '{' . self::NS_CALDAV . '}';
+
+ $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
+ if (!$principalUri) {
+ $iTipMessage->scheduleStatus = '3.7;Could not find principal.';
+ return;
+ }
+
+ // We found a principal URL, now we need to find its inbox.
+ // Unfortunately we may not have sufficient privileges to find this, so
+ // we are temporarily turning off ACL to let this come through.
+ //
+ // Once we support PHP 5.5, this should be wrapped in a try..finally
+ // block so we can ensure that this privilege gets added again after.
+ $this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
+
+ $result = $this->server->getProperties(
+ $principalUri,
+ [
+ '{DAV:}principal-URL',
+ $caldavNS . 'calendar-home-set',
+ $caldavNS . 'schedule-inbox-URL',
+ $caldavNS . 'schedule-default-calendar-URL',
+ '{http://sabredav.org/ns}email-address',
+ ]
+ );
+
+ // Re-registering the ACL event
+ $this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
+
+ if (!isset($result[$caldavNS . 'schedule-inbox-URL'])) {
+ $iTipMessage->scheduleStatus = '5.2;Could not find local inbox';
+ return;
+ }
+ if (!isset($result[$caldavNS . 'calendar-home-set'])) {
+ $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set';
+ return;
+ }
+ if (!isset($result[$caldavNS . 'schedule-default-calendar-URL'])) {
+ $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property';
+ return;
+ }
+
+ $calendarPath = $result[$caldavNS . 'schedule-default-calendar-URL']->getHref();
+ $homePath = $result[$caldavNS . 'calendar-home-set']->getHref();
+ $inboxPath = $result[$caldavNS . 'schedule-inbox-URL']->getHref();
+
+ if ($iTipMessage->method === 'REPLY') {
+ $privilege = 'schedule-deliver-reply';
+ } else {
+ $privilege = 'schedule-deliver-invite';
+ }
+
+ if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS . $privilege, DAVACL\Plugin::R_PARENT, false)) {
+ $iTipMessage->scheduleStatus = '3.8;organizer did not have the ' . $privilege . ' privilege on the attendees inbox';
+ return;
+ }
+
+ // Next, we're going to find out if the item already exits in one of
+ // the users' calendars.
+ $uid = $iTipMessage->uid;
+
+ $newFileName = 'sabredav-' . \Sabre\DAV\UUIDUtil::getUUID() . '.ics';
+
+ $home = $this->server->tree->getNodeForPath($homePath);
+ $inbox = $this->server->tree->getNodeForPath($inboxPath);
+
+ $currentObject = null;
+ $objectNode = null;
+ $isNewNode = false;
+
+ $result = $home->getCalendarObjectByUID($uid);
+ if ($result) {
+ // There was an existing object, we need to update probably.
+ $objectPath = $homePath . '/' . $result;
+ $objectNode = $this->server->tree->getNodeForPath($objectPath);
+ $oldICalendarData = $objectNode->get();
+ $currentObject = Reader::read($oldICalendarData);
+ } else {
+ $isNewNode = true;
+ }
+
+ $broker = new ITip\Broker();
+ $newObject = $broker->processMessage($iTipMessage, $currentObject);
+
+ $inbox->createFile($newFileName, $iTipMessage->message->serialize());
+
+ if (!$newObject) {
+ // We received an iTip message referring to a UID that we don't
+ // have in any calendars yet, and processMessage did not give us a
+ // calendarobject back.
+ //
+ // The implication is that processMessage did not understand the
+ // iTip message.
+ $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.';
+ return;
+ }
+
+ // Note that we are bypassing ACL on purpose by calling this directly.
+ // We may need to look a bit deeper into this later. Supporting ACL
+ // here would be nice.
+ if ($isNewNode) {
+ $calendar = $this->server->tree->getNodeForPath($calendarPath);
+ $calendar->createFile($newFileName, $newObject->serialize());
+ } else {
+ // If the message was a reply, we may have to inform other
+ // attendees of this attendees status. Therefore we're shooting off
+ // another itipMessage.
+ if ($iTipMessage->method === 'REPLY') {
+ $this->processICalendarChange(
+ $oldICalendarData,
+ $newObject,
+ [$iTipMessage->recipient],
+ [$iTipMessage->sender]
+ );
+ }
+ $objectNode->put($newObject->serialize());
+ }
+ $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
+
+ }
+
+ /**
+ * This method looks at an old iCalendar object, a new iCalendar object and
+ * starts sending scheduling messages based on the changes.
+ *
+ * A list of addresses needs to be specified, so the system knows who made
+ * the update, because the behavior may be different based on if it's an
+ * attendee or an organizer.
+ *
+ * This method may update $newObject to add any status changes.
+ *
+ * @param VCalendar|string $oldObject
+ * @param VCalendar $newObject
+ * @param array $addresses
+ * @param array $ignore Any addresses to not send messages to.
+ * @param bool $modified A marker to indicate that the original object
+ * modified by this process.
+ * @return void
+ */
+ protected function processICalendarChange($oldObject = null, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false) {
+
+ $broker = new ITip\Broker();
+ $messages = $broker->parseEvent($newObject, $addresses, $oldObject);
+
+ if ($messages) $modified = true;
+
+ foreach ($messages as $message) {
+
+ if (in_array($message->recipient, $ignore)) {
+ continue;
+ }
+
+ $this->deliver($message);
+
+ if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) {
+ if ($message->scheduleStatus) {
+ $newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus();
+ }
+ unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']);
+
+ } else {
+
+ if (isset($newObject->VEVENT->ATTENDEE)) foreach ($newObject->VEVENT->ATTENDEE as $attendee) {
+
+ if ($attendee->getNormalizedValue() === $message->recipient) {
+ if ($message->scheduleStatus) {
+ $attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus();
+ }
+ unset($attendee['SCHEDULE-FORCE-SEND']);
+ break;
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Returns a list of addresses that are associated with a principal.
+ *
+ * @param string $principal
+ * @return array
+ */
+ protected function getAddressesForPrincipal($principal) {
+
+ $CUAS = '{' . self::NS_CALDAV . '}calendar-user-address-set';
+
+ $properties = $this->server->getProperties(
+ $principal,
+ [$CUAS]
+ );
+
+ // If we can't find this information, we'll stop processing
+ if (!isset($properties[$CUAS])) {
+ return;
+ }
+
+ $addresses = $properties[$CUAS]->getHrefs();
+ return $addresses;
+
+ }
+
+ /**
+ * 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
+ */
+ 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 one type of request:
+ //
+ // REQUEST with a VFREEBUSY component
+
+ $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);
+
+ // Destroy circular references so PHP can GC the object.
+ $vObject->destroy();
+ unset($vObject);
+
+ } else {
+
+ throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint');
+
+ }
+
+ }
+
+ /**
+ * 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->xml->namespaceMap 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 DateTimeInterface $start
+ * @param DateTimeInterface $end
+ * @param VObject\Component $request
+ * @return array
+ */
+ protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request) {
+
+ $caldavNS = '{' . self::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',
+ $caldavNS . 'schedule-inbox-URL',
+ '{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,
+ ];
+ }
+ if (!isset($result[0][200][$caldavNS . 'schedule-inbox-URL'])) {
+ return [
+ 'request-status' => '3.7;No schedule-inbox-URL property found',
+ 'href' => 'mailto:' . $email,
+ ];
+ }
+ $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
+ $inboxUrl = $result[0][200][$caldavNS . 'schedule-inbox-URL']->getHref();
+
+ // Grabbing the calendar list
+ $objects = [];
+ $calendarTimeZone = new DateTimeZone('UTC');
+
+ foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
+ if (!$node instanceof ICalendar) {
+ continue;
+ }
+
+ $sct = $caldavNS . 'schedule-calendar-transp';
+ $ctz = $caldavNS . 'calendar-timezone';
+ $props = $node->getProperties([$sct, $ctz]);
+
+ if (isset($props[$sct]) && $props[$sct]->getValue() == ScheduleCalendarTransp::TRANSPARENT) {
+ // If a calendar is marked as 'transparent', it means we must
+ // ignore it for free-busy purposes.
+ continue;
+ }
+
+ $aclPlugin->checkPrivileges($homeSet . $node->getName(), $caldavNS . 'read-free-busy');
+
+ if (isset($props[$ctz])) {
+ $vtimezoneObj = VObject\Reader::read($props[$ctz]);
+ $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+
+ // Destroy circular references so PHP can garbage collect the object.
+ $vtimezoneObj->destroy();
+
+ }
+
+ // 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);
+
+ }
+
+ $inboxProps = $this->server->getProperties(
+ $inboxUrl,
+ $caldavNS . 'calendar-availability'
+ );
+
+ $vcalendar = new VObject\Component\VCalendar();
+ $vcalendar->METHOD = 'REPLY';
+
+ $generator = new VObject\FreeBusyGenerator();
+ $generator->setObjects($objects);
+ $generator->setTimeRange($start, $end);
+ $generator->setBaseObject($vcalendar);
+ $generator->setTimeZone($calendarTimeZone);
+
+ if ($inboxProps) {
+ $generator->setVAvailability(
+ VObject\Reader::read(
+ $inboxProps[$caldavNS . 'calendar-availability']
+ )
+ );
+ }
+
+ $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,
+ ];
+ }
+
+ /**
+ * This method checks the 'Schedule-Reply' header
+ * and returns false if it's 'F', otherwise true.
+ *
+ * @param RequestInterface $request
+ * @return bool
+ */
+ private function scheduleReply(RequestInterface $request) {
+
+ $scheduleReply = $request->getHeader('Schedule-Reply');
+ return $scheduleReply !== 'F';
+
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ function getPluginInfo() {
+
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Adds calendar-auto-schedule, as defined in rf6868',
+ 'link' => 'http://sabre.io/dav/scheduling/',
+ ];
+
+ }
+}
diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php b/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php
new file mode 100644
index 000000000..a36646e6c
--- /dev/null
+++ b/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use Sabre\CalDAV\Backend;
+use Sabre\DAV\Exception\MethodNotAllowed;
+
+/**
+ * The SchedulingObject represents a scheduling object in the Inbox collection
+ *
+ * @author Brett (https://github.com/bretten)
+ * @license http://sabre.io/license/ Modified BSD License
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ */
+class SchedulingObject extends \Sabre\CalDAV\CalendarObject implements ISchedulingObject {
+
+ /**
+ /* The CalDAV backend
+ *
+ * @var Backend\SchedulingSupport
+ */
+ protected $caldavBackend;
+
+ /**
+ * Array with information about this SchedulingObject
+ *
+ * @var array
+ */
+ protected $objectData;
+
+ /**
+ * Constructor
+ *
+ * The following properties may be passed within $objectData:
+ *
+ * * uri - A unique uri. Only the 'basename' must be passed.
+ * * principaluri - the principal that owns the object.
+ * * calendardata (optional) - The iCalendar data
+ * * etag - (optional) The etag for this object, MUST be encloded with
+ * double-quotes.
+ * * size - (optional) The size of the data in bytes.
+ * * lastmodified - (optional) format as a unix timestamp.
+ * * acl - (optional) Use this to override the default ACL for the node.
+ *
+ * @param Backend\BackendInterface $caldavBackend
+ * @param array $objectData
+ */
+ function __construct(Backend\SchedulingSupport $caldavBackend, array $objectData) {
+
+ $this->caldavBackend = $caldavBackend;
+
+ if (!isset($objectData['uri'])) {
+ throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
+ }
+
+ $this->objectData = $objectData;
+
+ }
+
+ /**
+ * Returns the ICalendar-formatted object
+ *
+ * @return string
+ */
+ function get() {
+
+ // Pre-populating the 'calendardata' is optional, if we don't have it
+ // already we fetch it from the backend.
+ if (!isset($this->objectData['calendardata'])) {
+ $this->objectData = $this->caldavBackend->getSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
+ }
+ return $this->objectData['calendardata'];
+
+ }
+
+ /**
+ * Updates the ICalendar-formatted object
+ *
+ * @param string|resource $calendarData
+ * @return string
+ */
+ function put($calendarData) {
+
+ throw new MethodNotAllowed('Updating scheduling objects is not supported');
+
+ }
+
+ /**
+ * Deletes the scheduling message
+ *
+ * @return void
+ */
+ function delete() {
+
+ $this->caldavBackend->deleteSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ function getOwner() {
+
+ return $this->objectData['principaluri'];
+
+ }
+
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ function getACL() {
+
+ // An alternative acl may be specified in the object data.
+ //
+
+ if (isset($this->objectData['acl'])) {
+ return $this->objectData['acl'];
+ }
+
+ // The default ACL
+ return [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->objectData['principaluri'],
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->objectData['principaluri'],
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->objectData['principaluri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->objectData['principaluri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->objectData['principaluri'] . '/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ ];
+
+ }
+
+}