2013-10-21 22:46:31 +00:00
< ? php
namespace Sabre\CalDAV ;
use Sabre\DAV ;
use Sabre\DAVACL ;
use Sabre\VObject ;
/**
* CalDAV plugin
*
* This plugin provides functionality added by CalDAV ( RFC 4791 )
* It implements new reports , and the MKCALENDAR method .
*
2014-04-12 17:13:37 +00:00
* @ copyright Copyright ( C ) 2007 - 2014 fruux GmbH ( https :// fruux . com / ) .
2013-10-21 22:46:31 +00:00
* @ author Evert Pot ( http :// evertpot . com / )
2014-06-28 20:28:08 +00:00
* @ license http :// sabre . io / license / Modified BSD License
2013-10-21 22:46:31 +00:00
*/
class Plugin extends DAV\ServerPlugin {
/**
* This is the official CalDAV namespace
*/
const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav' ;
/**
* This is the namespace for the proprietary calendarserver extensions
*/
const NS_CALENDARSERVER = 'http://calendarserver.org/ns/' ;
/**
* The hardcoded root for calendar objects . It is unfortunate
* that we ' re stuck with it , but it will have to do for now
*/
const CALENDAR_ROOT = 'calendars' ;
/**
* Reference to server object
*
* @ var DAV\Server
*/
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 .
*
* 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 ) {
// The MKCALENDAR is only available on unmapped uri's, whose
// parents extend IExtendedCollection
list ( $parent , $name ) = DAV\URLUtil :: splitPath ( $uri );
$node = $this -> server -> tree -> getNodeForPath ( $parent );
if ( $node instanceof DAV\IExtendedCollection ) {
try {
$node -> getChild ( $name );
} catch ( DAV\Exception\NotFound $e ) {
return array ( 'MKCALENDAR' );
}
}
return array ();
}
/**
* Returns a list of features for the DAV : HTTP header .
*
* @ return array
*/
public function getFeatures () {
return array ( 'calendar-access' , 'calendar-proxy' );
}
/**
* Returns a plugin name .
*
* Using this name other plugins will be able to access other plugins
* using DAV\Server :: getPlugin
*
* @ return string
*/
public function getPluginName () {
return 'caldav' ;
}
/**
* Returns a list of reports this plugin supports .
*
* This will be used in the { DAV : } supported - report - set property .
* Note that you still need to subscribe to the 'report' event to actually
* implement them
*
* @ param string $uri
* @ return array
*/
public function getSupportedReportSet ( $uri ) {
$node = $this -> server -> tree -> getNodeForPath ( $uri );
$reports = array ();
if ( $node instanceof ICalendar || $node instanceof ICalendarObject ) {
$reports [] = '{' . self :: NS_CALDAV . '}calendar-multiget' ;
$reports [] = '{' . self :: NS_CALDAV . '}calendar-query' ;
}
if ( $node instanceof ICalendar ) {
$reports [] = '{' . self :: NS_CALDAV . '}free-busy-query' ;
}
return $reports ;
}
/**
* Initializes the plugin
*
* @ param DAV\Server $server
* @ return void
*/
public function initialize ( DAV\Server $server ) {
$this -> server = $server ;
$server -> subscribeEvent ( 'unknownMethod' , array ( $this , 'unknownMethod' ));
//$server->subscribeEvent('unknownMethod',array($this,'unknownMethod2'),1000);
$server -> subscribeEvent ( 'report' , array ( $this , 'report' ));
$server -> subscribeEvent ( 'beforeGetProperties' , array ( $this , 'beforeGetProperties' ));
$server -> subscribeEvent ( 'onHTMLActionsPanel' , array ( $this , 'htmlActionsPanel' ));
$server -> subscribeEvent ( 'onBrowserPostAction' , array ( $this , 'browserPostAction' ));
$server -> subscribeEvent ( 'beforeWriteContent' , array ( $this , 'beforeWriteContent' ));
$server -> subscribeEvent ( 'beforeCreateFile' , array ( $this , 'beforeCreateFile' ));
$server -> subscribeEvent ( 'beforeMethod' , array ( $this , 'beforeMethod' ));
$server -> xmlNamespaces [ self :: NS_CALDAV ] = 'cal' ;
$server -> xmlNamespaces [ self :: NS_CALENDARSERVER ] = 'cs' ;
$server -> propertyMap [ '{' . self :: NS_CALDAV . '}supported-calendar-component-set' ] = 'Sabre\\CalDAV\\Property\\SupportedCalendarComponentSet' ;
$server -> propertyMap [ '{' . self :: NS_CALDAV . '}schedule-calendar-transp' ] = 'Sabre\\CalDAV\\Property\\ScheduleCalendarTransp' ;
$server -> resourceTypeMapping [ '\\Sabre\\CalDAV\\ICalendar' ] = '{urn:ietf:params:xml:ns:caldav}calendar' ;
$server -> resourceTypeMapping [ '\\Sabre\\CalDAV\\Schedule\\IOutbox' ] = '{urn:ietf:params:xml:ns:caldav}schedule-outbox' ;
$server -> resourceTypeMapping [ '\\Sabre\\CalDAV\\Principal\\IProxyRead' ] = '{http://calendarserver.org/ns/}calendar-proxy-read' ;
$server -> resourceTypeMapping [ '\\Sabre\\CalDAV\\Principal\\IProxyWrite' ] = '{http://calendarserver.org/ns/}calendar-proxy-write' ;
$server -> resourceTypeMapping [ '\\Sabre\\CalDAV\\Notifications\\ICollection' ] = '{' . self :: NS_CALENDARSERVER . '}notification' ;
array_push ( $server -> protectedProperties ,
'{' . self :: NS_CALDAV . '}supported-calendar-component-set' ,
'{' . self :: NS_CALDAV . '}supported-calendar-data' ,
'{' . self :: NS_CALDAV . '}max-resource-size' ,
'{' . self :: NS_CALDAV . '}min-date-time' ,
'{' . self :: NS_CALDAV . '}max-date-time' ,
'{' . self :: NS_CALDAV . '}max-instances' ,
'{' . self :: NS_CALDAV . '}max-attendees-per-instance' ,
'{' . self :: NS_CALDAV . '}calendar-home-set' ,
'{' . self :: NS_CALDAV . '}supported-collation-set' ,
'{' . self :: NS_CALDAV . '}calendar-data' ,
// scheduling extension
'{' . self :: NS_CALDAV . '}schedule-inbox-URL' ,
'{' . self :: NS_CALDAV . '}schedule-outbox-URL' ,
'{' . self :: NS_CALDAV . '}calendar-user-address-set' ,
'{' . self :: NS_CALDAV . '}calendar-user-type' ,
// CalendarServer extensions
'{' . self :: NS_CALENDARSERVER . '}getctag' ,
'{' . self :: NS_CALENDARSERVER . '}calendar-proxy-read-for' ,
'{' . self :: NS_CALENDARSERVER . '}calendar-proxy-write-for' ,
'{' . self :: NS_CALENDARSERVER . '}notification-URL' ,
'{' . self :: NS_CALENDARSERVER . '}notificationtype'
);
}
/**
* This function handles support for the MKCALENDAR method
*
* @ param string $method
* @ param string $uri
* @ return bool
*/
public function unknownMethod ( $method , $uri ) {
switch ( $method ) {
case 'MKCALENDAR' :
$this -> httpMkCalendar ( $uri );
// false is returned to stop the propagation of the
// unknownMethod event.
return false ;
case 'POST' :
// Checking if this is a text/calendar content type
$contentType = $this -> server -> httpRequest -> getHeader ( 'Content-Type' );
if ( strpos ( $contentType , 'text/calendar' ) !== 0 ) {
return ;
}
// Checking if we're talking to an outbox
try {
$node = $this -> server -> tree -> getNodeForPath ( $uri );
} catch ( DAV\Exception\NotFound $e ) {
return ;
}
if ( ! $node instanceof Schedule\IOutbox )
return ;
$this -> outboxRequest ( $node , $uri );
return false ;
}
}
/**
* This functions handles REPORT requests specific to CalDAV
*
* @ param string $reportName
* @ param \DOMNode $dom
* @ return bool
*/
public function report ( $reportName , $dom ) {
switch ( $reportName ) {
case '{' . self :: NS_CALDAV . '}calendar-multiget' :
$this -> calendarMultiGetReport ( $dom );
return false ;
case '{' . self :: NS_CALDAV . '}calendar-query' :
$this -> calendarQueryReport ( $dom );
return false ;
case '{' . self :: NS_CALDAV . '}free-busy-query' :
$this -> freeBusyQueryReport ( $dom );
return false ;
}
}
/**
* This function handles the MKCALENDAR HTTP method , which creates
* a new calendar .
*
* @ param string $uri
* @ return void
*/
public function httpMkCalendar ( $uri ) {
// Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support
// for clients matching iCal in the user agent
//$ua = $this->server->httpRequest->getHeader('User-Agent');
//if (strpos($ua,'iCal/')!==false) {
// throw new \Sabre\DAV\Exception\Forbidden('iCal has major bugs in it\'s RFC3744 support. Therefore we are left with no other choice but disabling this feature.');
//}
$body = $this -> server -> httpRequest -> getBody ( true );
$properties = array ();
if ( $body ) {
$dom = DAV\XMLUtil :: loadDOMDocument ( $body );
foreach ( $dom -> firstChild -> childNodes as $child ) {
if ( DAV\XMLUtil :: toClarkNotation ( $child ) !== '{DAV:}set' ) continue ;
foreach ( DAV\XMLUtil :: parseProperties ( $child , $this -> server -> propertyMap ) as $k => $prop ) {
$properties [ $k ] = $prop ;
}
}
}
$resourceType = array ( '{DAV:}collection' , '{urn:ietf:params:xml:ns:caldav}calendar' );
$this -> server -> createCollection ( $uri , $resourceType , $properties );
$this -> server -> httpResponse -> sendStatus ( 201 );
$this -> server -> httpResponse -> setHeader ( 'Content-Length' , 0 );
}
/**
* beforeGetProperties
*
* This method handler is invoked before any after properties for a
* resource are fetched . This allows us to add in any CalDAV specific
* properties .
*
* @ param string $path
* @ param DAV\INode $node
* @ param array $requestedProperties
* @ param array $returnedProperties
* @ return void
*/
public function beforeGetProperties ( $path , DAV\INode $node , & $requestedProperties , & $returnedProperties ) {
if ( $node instanceof DAVACL\IPrincipal ) {
// calendar-home-set property
$calHome = '{' . self :: NS_CALDAV . '}calendar-home-set' ;
if ( in_array ( $calHome , $requestedProperties )) {
$principalId = $node -> getName ();
$calendarHomePath = self :: CALENDAR_ROOT . '/' . $principalId . '/' ;
unset ( $requestedProperties [ array_search ( $calHome , $requestedProperties )]);
$returnedProperties [ 200 ][ $calHome ] = new DAV\Property\Href ( $calendarHomePath );
}
// schedule-outbox-URL property
$scheduleProp = '{' . self :: NS_CALDAV . '}schedule-outbox-URL' ;
if ( in_array ( $scheduleProp , $requestedProperties )) {
$principalId = $node -> getName ();
$outboxPath = self :: CALENDAR_ROOT . '/' . $principalId . '/outbox' ;
unset ( $requestedProperties [ array_search ( $scheduleProp , $requestedProperties )]);
$returnedProperties [ 200 ][ $scheduleProp ] = new DAV\Property\Href ( $outboxPath );
}
// calendar-user-address-set property
$calProp = '{' . self :: NS_CALDAV . '}calendar-user-address-set' ;
if ( in_array ( $calProp , $requestedProperties )) {
$addresses = $node -> getAlternateUriSet ();
$addresses [] = $this -> server -> getBaseUri () . DAV\URLUtil :: encodePath ( $node -> getPrincipalUrl () . '/' );
unset ( $requestedProperties [ array_search ( $calProp , $requestedProperties )]);
$returnedProperties [ 200 ][ $calProp ] = new DAV\Property\HrefList ( $addresses , false );
}
// These two properties are shortcuts for ical to easily find
// other principals this principal has access to.
$propRead = '{' . self :: NS_CALENDARSERVER . '}calendar-proxy-read-for' ;
$propWrite = '{' . self :: NS_CALENDARSERVER . '}calendar-proxy-write-for' ;
if ( in_array ( $propRead , $requestedProperties ) || in_array ( $propWrite , $requestedProperties )) {
$aclPlugin = $this -> server -> getPlugin ( 'acl' );
$membership = $aclPlugin -> getPrincipalMembership ( $path );
$readList = array ();
$writeList = array ();
foreach ( $membership as $group ) {
$groupNode = $this -> server -> tree -> getNodeForPath ( $group );
// If the node is either ap proxy-read or proxy-write
// group, we grab the parent principal and add it to the
// list.
if ( $groupNode instanceof Principal\IProxyRead ) {
list ( $readList []) = DAV\URLUtil :: splitPath ( $group );
}
if ( $groupNode instanceof Principal\IProxyWrite ) {
list ( $writeList []) = DAV\URLUtil :: splitPath ( $group );
}
}
if ( in_array ( $propRead , $requestedProperties )) {
unset ( $requestedProperties [ $propRead ]);
$returnedProperties [ 200 ][ $propRead ] = new DAV\Property\HrefList ( $readList );
}
if ( in_array ( $propWrite , $requestedProperties )) {
unset ( $requestedProperties [ $propWrite ]);
$returnedProperties [ 200 ][ $propWrite ] = new DAV\Property\HrefList ( $writeList );
}
}
// notification-URL property
$notificationUrl = '{' . self :: NS_CALENDARSERVER . '}notification-URL' ;
if (( $index = array_search ( $notificationUrl , $requestedProperties )) !== false ) {
$principalId = $node -> getName ();
$calendarHomePath = 'calendars/' . $principalId . '/notifications/' ;
unset ( $requestedProperties [ $index ]);
$returnedProperties [ 200 ][ $notificationUrl ] = new DAV\Property\Href ( $calendarHomePath );
}
} // instanceof IPrincipal
if ( $node instanceof Notifications\INode ) {
$propertyName = '{' . self :: NS_CALENDARSERVER . '}notificationtype' ;
if (( $index = array_search ( $propertyName , $requestedProperties )) !== false ) {
$returnedProperties [ 200 ][ $propertyName ] =
$node -> getNotificationType ();
unset ( $requestedProperties [ $index ]);
}
} // instanceof Notifications_INode
if ( $node instanceof ICalendarObject ) {
// The calendar-data property is not supposed to be a 'real'
// property, but in large chunks of the spec it does act as such.
// Therefore we simply expose it as a property.
$calDataProp = '{' . Plugin :: NS_CALDAV . '}calendar-data' ;
if ( in_array ( $calDataProp , $requestedProperties )) {
unset ( $requestedProperties [ $calDataProp ]);
$val = $node -> get ();
if ( is_resource ( $val ))
$val = stream_get_contents ( $val );
// Taking out \r to not screw up the xml output
$returnedProperties [ 200 ][ $calDataProp ] = str_replace ( " \r " , " " , $val );
}
}
}
/**
* This function handles the calendar - multiget REPORT .
*
* This report is used by the client to fetch the content of a series
* of urls . Effectively avoiding a lot of redundant requests .
*
* @ param \DOMNode $dom
* @ return void
*/
public function calendarMultiGetReport ( $dom ) {
$properties = array_keys ( DAV\XMLUtil :: parseProperties ( $dom -> firstChild ));
$hrefElems = $dom -> getElementsByTagNameNS ( 'urn:DAV' , 'href' );
$xpath = new \DOMXPath ( $dom );
$xpath -> registerNameSpace ( 'cal' , Plugin :: NS_CALDAV );
$xpath -> registerNameSpace ( 'dav' , 'urn:DAV' );
$expand = $xpath -> query ( '/cal:calendar-multiget/dav:prop/cal:calendar-data/cal:expand' );
if ( $expand -> length > 0 ) {
$expandElem = $expand -> item ( 0 );
$start = $expandElem -> getAttribute ( 'start' );
$end = $expandElem -> getAttribute ( 'end' );
if ( ! $start || ! $end ) {
throw new DAV\Exception\BadRequest ( 'The "start" and "end" attributes are required for the CALDAV:expand element' );
}
$start = VObject\DateTimeParser :: parseDateTime ( $start );
$end = VObject\DateTimeParser :: parseDateTime ( $end );
if ( $end <= $start ) {
throw new DAV\Exception\BadRequest ( 'The end-date must be larger than the start-date in the expand element.' );
}
$expand = true ;
} else {
$expand = false ;
}
foreach ( $hrefElems as $elem ) {
$uri = $this -> server -> calculateUri ( $elem -> nodeValue );
list ( $objProps ) = $this -> server -> getPropertiesForPath ( $uri , $properties );
if ( $expand && isset ( $objProps [ 200 ][ '{' . self :: NS_CALDAV . '}calendar-data' ])) {
$vObject = VObject\Reader :: read ( $objProps [ 200 ][ '{' . self :: NS_CALDAV . '}calendar-data' ]);
$vObject -> expand ( $start , $end );
$objProps [ 200 ][ '{' . self :: NS_CALDAV . '}calendar-data' ] = $vObject -> serialize ();
}
$propertyList [] = $objProps ;
}
$prefer = $this -> server -> getHTTPPRefer ();
$this -> server -> httpResponse -> sendStatus ( 207 );
$this -> server -> httpResponse -> setHeader ( 'Content-Type' , 'application/xml; charset=utf-8' );
$this -> server -> httpResponse -> setHeader ( 'Vary' , 'Brief,Prefer' );
$this -> server -> httpResponse -> sendBody ( $this -> server -> generateMultiStatus ( $propertyList , $prefer [ 'return-minimal' ]));
}
/**
* This function handles the calendar - query REPORT
*
* This report is used by clients to request calendar objects based on
* complex conditions .
*
* @ param \DOMNode $dom
* @ return void
*/
public function calendarQueryReport ( $dom ) {
$parser = new CalendarQueryParser ( $dom );
$parser -> parse ();
$node = $this -> server -> tree -> getNodeForPath ( $this -> server -> getRequestUri ());
$depth = $this -> server -> getHTTPDepth ( 0 );
// The default result is an empty array
$result = array ();
// The calendarobject was requested directly. In this case we handle
// this locally.
if ( $depth == 0 && $node instanceof ICalendarObject ) {
$requestedCalendarData = true ;
$requestedProperties = $parser -> requestedProperties ;
if ( ! in_array ( '{urn:ietf:params:xml:ns:caldav}calendar-data' , $requestedProperties )) {
// We always retrieve calendar-data, as we need it for filtering.
$requestedProperties [] = '{urn:ietf:params:xml:ns:caldav}calendar-data' ;
// If calendar-data wasn't explicitly requested, we need to remove
// it after processing.
$requestedCalendarData = false ;
}
$properties = $this -> server -> getPropertiesForPath (
$this -> server -> getRequestUri (),
$requestedProperties ,
0
);
// This array should have only 1 element, the first calendar
// object.
$properties = current ( $properties );
// If there wasn't any calendar-data returned somehow, we ignore
// this.
if ( isset ( $properties [ 200 ][ '{urn:ietf:params:xml:ns:caldav}calendar-data' ])) {
$validator = new CalendarQueryValidator ();
$vObject = VObject\Reader :: read ( $properties [ 200 ][ '{urn:ietf:params:xml:ns:caldav}calendar-data' ]);
if ( $validator -> validate ( $vObject , $parser -> filters )) {
// If the client didn't require the calendar-data property,
// we won't give it back.
if ( ! $requestedCalendarData ) {
unset ( $properties [ 200 ][ '{urn:ietf:params:xml:ns:caldav}calendar-data' ]);
} else {
if ( $parser -> expand ) {
$vObject -> expand ( $parser -> expand [ 'start' ], $parser -> expand [ 'end' ]);
$properties [ 200 ][ '{' . self :: NS_CALDAV . '}calendar-data' ] = $vObject -> serialize ();
}
}
$result = array ( $properties );
}
}
}
// If we're dealing with a calendar, the calendar itself is responsible
// for the calendar-query.
if ( $node instanceof ICalendar && $depth = 1 ) {
$nodePaths = $node -> calendarQuery ( $parser -> filters );
foreach ( $nodePaths as $path ) {
list ( $properties ) =
$this -> server -> getPropertiesForPath ( $this -> server -> getRequestUri () . '/' . $path , $parser -> requestedProperties );
if ( $parser -> expand ) {
// We need to do some post-processing
$vObject = VObject\Reader :: read ( $properties [ 200 ][ '{urn:ietf:params:xml:ns:caldav}calendar-data' ]);
$vObject -> expand ( $parser -> expand [ 'start' ], $parser -> expand [ 'end' ]);
$properties [ 200 ][ '{' . self :: NS_CALDAV . '}calendar-data' ] = $vObject -> serialize ();
}
$result [] = $properties ;
}
}
$prefer = $this -> server -> getHTTPPRefer ();
$this -> server -> httpResponse -> sendStatus ( 207 );
$this -> server -> httpResponse -> setHeader ( 'Content-Type' , 'application/xml; charset=utf-8' );
$this -> server -> httpResponse -> setHeader ( 'Vary' , 'Brief,Prefer' );
$this -> server -> httpResponse -> sendBody ( $this -> server -> generateMultiStatus ( $result , $prefer [ 'return-minimal' ]));
}
/**
* This method is responsible for parsing the request and generating the
* response for the CALDAV : free - busy - query REPORT .
*
* @ param \DOMNode $dom
* @ return void
*/
protected function freeBusyQueryReport ( \DOMNode $dom ) {
$start = null ;
$end = null ;
foreach ( $dom -> firstChild -> childNodes as $childNode ) {
$clark = DAV\XMLUtil :: toClarkNotation ( $childNode );
if ( $clark == '{' . self :: NS_CALDAV . '}time-range' ) {
$start = $childNode -> getAttribute ( 'start' );
$end = $childNode -> getAttribute ( 'end' );
break ;
}
}
if ( $start ) {
$start = VObject\DateTimeParser :: parseDateTime ( $start );
}
if ( $end ) {
$end = VObject\DateTimeParser :: parseDateTime ( $end );
}
if ( ! $start && ! $end ) {
throw new DAV\Exception\BadRequest ( 'The freebusy report must have a time-range filter' );
}
$acl = $this -> server -> getPlugin ( 'acl' );
if ( ! $acl ) {
throw new DAV\Exception ( 'The ACL plugin must be loaded for free-busy queries to work' );
}
$uri = $this -> server -> getRequestUri ();
$acl -> checkPrivileges ( $uri , '{' . self :: NS_CALDAV . '}read-free-busy' );
$calendar = $this -> server -> tree -> getNodeForPath ( $uri );
if ( ! $calendar instanceof ICalendar ) {
throw new DAV\Exception\NotImplemented ( 'The free-busy-query REPORT is only implemented on calendars' );
}
// Doing a calendar-query first, to make sure we get the most
// performance.
$urls = $calendar -> 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 ,
));
$objects = array_map ( function ( $url ) use ( $calendar ) {
$obj = $calendar -> getChild ( $url ) -> get ();
return $obj ;
}, $urls );
$generator = new VObject\FreeBusyGenerator ();
$generator -> setObjects ( $objects );
$generator -> setTimeRange ( $start , $end );
$result = $generator -> getResult ();
$result = $result -> serialize ();
$this -> server -> httpResponse -> sendStatus ( 200 );
$this -> server -> httpResponse -> setHeader ( 'Content-Type' , 'text/calendar' );
$this -> server -> httpResponse -> setHeader ( 'Content-Length' , strlen ( $result ));
$this -> server -> httpResponse -> sendBody ( $result );
}
/**
* This method is triggered before a file gets updated with new content .
*
* This plugin uses this method to ensure that CalDAV objects receive
* valid calendar data .
*
* @ param string $path
* @ param DAV\IFile $node
* @ param resource $data
* @ return void
*/
public function beforeWriteContent ( $path , DAV\IFile $node , & $data ) {
if ( ! $node instanceof ICalendarObject )
return ;
$this -> validateICalendar ( $data , $path );
}
/**
* This method is triggered before a new file is created .
*
* This plugin uses this method to ensure that newly created calendar
* objects contain valid calendar data .
*
* @ param string $path
* @ param resource $data
* @ param DAV\ICollection $parentNode
* @ return void
*/
public function beforeCreateFile ( $path , & $data , DAV\ICollection $parentNode ) {
if ( ! $parentNode instanceof Calendar )
return ;
$this -> validateICalendar ( $data , $path );
}
/**
* This event is triggered before any HTTP request is handled .
*
* We use this to intercept GET calls to notification nodes , and return the
* proper response .
*
* @ param string $method
* @ param string $path
* @ return void
*/
public function beforeMethod ( $method , $path ) {
if ( $method !== 'GET' ) return ;
try {
$node = $this -> server -> tree -> getNodeForPath ( $path );
} catch ( DAV\Exception\NotFound $e ) {
return ;
}
if ( ! $node instanceof Notifications\INode )
return ;
if ( ! $this -> server -> checkPreconditions ( true )) return false ;
$dom = new \DOMDocument ( '1.0' , 'UTF-8' );
$dom -> formatOutput = true ;
$root = $dom -> createElement ( 'cs:notification' );
foreach ( $this -> server -> xmlNamespaces as $namespace => $prefix ) {
$root -> setAttribute ( 'xmlns:' . $prefix , $namespace );
}
$dom -> appendChild ( $root );
$node -> getNotificationType () -> serializeBody ( $this -> server , $root );
$this -> server -> httpResponse -> setHeader ( 'Content-Type' , 'application/xml' );
$this -> server -> httpResponse -> setHeader ( 'ETag' , $node -> getETag ());
$this -> server -> httpResponse -> sendStatus ( 200 );
$this -> server -> httpResponse -> sendBody ( $dom -> saveXML ());
return false ;
}
/**
* Checks if the submitted iCalendar data is in fact , valid .
*
* An exception is thrown if it ' s not .
*
* @ param resource | string $data
* @ param string $path
* @ return void
*/
protected function validateICalendar ( & $data , $path ) {
// If it's a stream, we convert it to a string first.
if ( is_resource ( $data )) {
$data = stream_get_contents ( $data );
}
// Converting the data to unicode, if needed.
$data = DAV\StringUtil :: ensureUTF8 ( $data );
try {
$vobj = VObject\Reader :: read ( $data );
} catch ( VObject\ParseException $e ) {
throw new DAV\Exception\UnsupportedMediaType ( 'This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e -> getMessage ());
}
if ( $vobj -> name !== 'VCALENDAR' ) {
throw new DAV\Exception\UnsupportedMediaType ( 'This collection can only support iCalendar objects.' );
}
// Get the Supported Components for the target calendar
list ( $parentPath , $object ) = DAV\URLUtil :: splitPath ( $path );
$calendarProperties = $this -> server -> getProperties ( $parentPath , array ( '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' ));
$supportedComponents = $calendarProperties [ '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' ] -> getValue ();
$foundType = null ;
$foundUID = null ;
foreach ( $vobj -> getComponents () as $component ) {
switch ( $component -> name ) {
case 'VTIMEZONE' :
continue 2 ;
case 'VEVENT' :
case 'VTODO' :
case 'VJOURNAL' :
if ( is_null ( $foundType )) {
$foundType = $component -> name ;
if ( ! in_array ( $foundType , $supportedComponents )) {
throw new Exception\InvalidComponentType ( 'This calendar only supports ' . implode ( ', ' , $supportedComponents ) . '. We found a ' . $foundType );
}
if ( ! isset ( $component -> UID )) {
throw new DAV\Exception\BadRequest ( 'Every ' . $component -> name . ' component must have an UID' );
}
$foundUID = ( string ) $component -> UID ;
} else {
if ( $foundType !== $component -> name ) {
throw new DAV\Exception\BadRequest ( 'A calendar object must only contain 1 component. We found a ' . $component -> name . ' as well as a ' . $foundType );
}
if ( $foundUID !== ( string ) $component -> UID ) {
throw new DAV\Exception\BadRequest ( 'Every ' . $component -> name . ' in this object must have identical UIDs' );
}
}
break ;
default :
throw new DAV\Exception\BadRequest ( 'You are not allowed to create components of type: ' . $component -> name . ' here' );
}
}
if ( ! $foundType )
throw new DAV\Exception\BadRequest ( 'iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL' );
}
/**
* 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 one of the email
// addresses of the selected principal.
$principal = $outboxNode -> getOwner ();
$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 -> sendStatus ( 200 );
$this -> server -> httpResponse -> setHeader ( 'Content-Type' , 'application/xml' );
$this -> server -> httpResponse -> sendBody ( $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@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 -> sendStatus ( 200 );
$this -> server -> httpResponse -> setHeader ( 'Content-Type' , 'application/xml' );
$this -> server -> httpResponse -> sendBody ( $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
* DAV\Browser\Plugin . This allows us to generate an interface users
* can use to create new calendars .
*
* @ param DAV\INode $node
* @ param string $output
* @ return bool
*/
public function htmlActionsPanel ( DAV\INode $node , & $output ) {
if ( ! $node instanceof UserCalendars )
return ;
$output .= ' < tr >< td colspan = " 2 " >< form method = " post " action = " " >
< h3 > Create new calendar </ h3 >
< input type = " hidden " name = " sabreAction " value = " mkcalendar " />
< label > Name ( uri ) :</ label > < input type = " text " name = " name " />< br />
< label > Display name :</ label > < input type = " text " name = " { DAV:}displayname " />< br />
< input type = " submit " value = " create " />
</ form >
</ td ></ tr > ' ;
return false ;
}
/**
* This method allows us to intercept the 'mkcalendar' sabreAction . This
* action enables the user to create new calendars from the browser plugin .
*
* @ param string $uri
* @ param string $action
* @ param array $postVars
* @ return bool
*/
public function browserPostAction ( $uri , $action , array $postVars ) {
if ( $action !== 'mkcalendar' )
return ;
$resourceType = array ( '{DAV:}collection' , '{urn:ietf:params:xml:ns:caldav}calendar' );
$properties = array ();
if ( isset ( $postVars [ '{DAV:}displayname' ])) {
$properties [ '{DAV:}displayname' ] = $postVars [ '{DAV:}displayname' ];
}
$this -> server -> createCollection ( $uri . '/' . $postVars [ 'name' ], $resourceType , $properties );
return false ;
}
}