Fix default URL for FindPrincipal() ensure If-Match etag is quoted.
[davical.git] / inc / caldav-client-v2.php
blob9d4db7d6f056697e778ea58c5a1ab867f56de3e4
1 <?php
2 /**
3 * A Class for connecting to a caldav server
5 * @package awl
7 * @subpackage caldav
8 * @author Andrew McMillan <andrew@mcmillan.net.nz>
9 * @copyright Andrew McMillan
10 * @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL version 3 or later
13 require_once('XMLDocument.php');
15 /**
16 * A class for holding basic calendar information
17 * @package awl
19 class CalendarInfo {
20 public $url, $displayname, $getctag;
22 function __construct( $url, $displayname = null, $getctag = null ) {
23 $this->url = $url;
24 $this->displayname = $displayname;
25 $this->getctag = $getctag;
28 function __toString() {
29 return( '(URL: '.$this->url.' Ctag: '.$this->getctag.' Displayname: '.$this->displayname .')'. "\n" );
34 /**
35 * A class for accessing DAViCal via CalDAV, as a client
37 * @package awl
39 class CalDAVClient {
40 /**
41 * Server, username, password, calendar
43 * @var string
45 protected $base_url, $user, $pass, $entry, $protocol, $server, $port;
47 /**
48 * The principal-URL we're using
50 protected $principal_url;
52 /**
53 * The calendar-URL we're using
55 protected $calendar_url;
57 /**
58 * The calendar-home-set we're using
60 protected $calendar_home_set;
62 /**
63 * The calendar_urls we have discovered
65 protected $calendar_urls;
67 /**
68 * The useragent which is send to the caldav server
70 * @var string
72 public $user_agent = 'DAViCalClient';
74 protected $headers = array();
75 protected $body = "";
76 protected $requestMethod = "GET";
77 protected $httpRequest = ""; // for debugging http headers sent
78 protected $xmlRequest = ""; // for debugging xml sent
79 protected $httpResponse = ""; // http headers received
80 protected $xmlResponse = ""; // xml received
82 protected $parser; // our XML parser object
84 /**
85 * Constructor, initialises the class
87 * @param string $base_url The URL for the calendar server
88 * @param string $user The name of the user logging in
89 * @param string $pass The password for that user
91 function __construct( $base_url, $user, $pass ) {
92 $this->user = $user;
93 $this->pass = $pass;
94 $this->headers = array();
96 if ( preg_match( '#^(https?)://([a-z0-9.-]+)(:([0-9]+))?(/.*)$#', $base_url, $matches ) ) {
97 $this->server = $matches[2];
98 $this->base_url = $matches[5];
99 if ( $matches[1] == 'https' ) {
100 $this->protocol = 'ssl';
101 $this->port = 443;
103 else {
104 $this->protocol = 'tcp';
105 $this->port = 80;
107 if ( $matches[4] != '' ) {
108 $this->port = intval($matches[4]);
111 else {
112 trigger_error("Invalid URL: '".$base_url."'", E_USER_ERROR);
117 * Adds an If-Match or If-None-Match header
119 * @param bool $match to Match or Not to Match, that is the question!
120 * @param string $etag The etag to match / not match against.
122 function SetMatch( $match, $etag = '*' ) {
123 $this->headers['match'] = sprintf( "%s-Match: \"%s\"", ($match ? "If" : "If-None"), trim($etag,'"'));
127 * Add a Depth: header. Valid values are 0, 1 or infinity
129 * @param int $depth The depth, default to infinity
131 function SetDepth( $depth = '0' ) {
132 $this->headers['depth'] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") );
136 * Add a Depth: header. Valid values are 1 or infinity
138 * @param int $depth The depth, default to infinity
140 function SetUserAgent( $user_agent = null ) {
141 if ( !isset($user_agent) ) $user_agent = $this->user_agent;
142 $this->user_agent = $user_agent;
146 * Add a Content-type: header.
148 * @param string $type The content type
150 function SetContentType( $type ) {
151 $this->headers['content-type'] = "Content-type: $type";
155 * Set the calendar_url we will be using for a while.
157 * @param string $url The calendar_url
159 function SetCalendar( $url ) {
160 $this->calendar_url = $url;
164 * Split response into httpResponse and xmlResponse
166 * @param string Response from server
168 function ParseResponse( $response ) {
169 $pos = strpos($response, '<?xml');
170 if ($pos === false) {
171 $this->httpResponse = trim($response);
173 else {
174 $this->httpResponse = trim(substr($response, 0, $pos));
175 $this->xmlResponse = trim(substr($response, $pos));
176 $this->xmlResponse = preg_replace('{>[^>]*$}s', '>',$this->xmlResponse );
177 $parser = xml_parser_create_ns('UTF-8');
178 xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 );
179 xml_parser_set_option ( $parser, XML_OPTION_CASE_FOLDING, 0 );
181 if ( xml_parse_into_struct( $parser, $this->xmlResponse, $this->xmlnodes, $this->xmltags ) === 0 ) {
182 printf( "XML parsing error: %s - %s\n", xml_get_error_code($parser), xml_error_string(xml_get_error_code($parser)) );
183 // debug_print_backtrace();
184 // echo "\nNodes array............................................................\n"; print_r( $this->xmlnodes );
185 // echo "\nTags array............................................................\n"; print_r( $this->xmltags );
186 printf( "\nXML Reponse:\n%s\n", $this->xmlResponse );
189 xml_parser_free($parser);
194 * Output http request headers
196 * @return HTTP headers
198 function GetHttpRequest() {
199 return $this->httpRequest;
202 * Output http response headers
204 * @return HTTP headers
206 function GetResponseHeaders() {
207 return $this->httpResponseHeaders;
210 * Output http response body
212 * @return HTTP body
214 function GetResponseBody() {
215 return $this->httpResponseBody;
218 * Output xml request
220 * @return raw xml
222 function GetXmlRequest() {
223 return $this->xmlRequest;
226 * Output xml response
228 * @return raw xml
230 function GetXmlResponse() {
231 return $this->xmlResponse;
235 * Send a request to the server
237 * @param string $url The URL to make the request to
239 * @return string The content of the response from the server
241 function DoRequest( $url = null ) {
242 if(!defined("_FSOCK_TIMEOUT")){ define("_FSOCK_TIMEOUT", 10); }
243 $headers = array();
245 if ( !isset($url) ) $url = $this->base_url;
246 $this->request_url = $url;
247 $url = preg_replace('{^https?://[^/]+}', '', $url);
248 // URLencode if it isn't already
249 if ( preg_match( '{[^%?&=+,.-_/a-z0-9]}', $url ) ) {
250 $url = str_replace(rawurlencode('/'),'/',rawurlencode($url));
251 $url = str_replace(rawurlencode('?'),'?',$url);
252 $url = str_replace(rawurlencode('&'),'&',$url);
253 $url = str_replace(rawurlencode('='),'=',$url);
254 $url = str_replace(rawurlencode('+'),'+',$url);
255 $url = str_replace(rawurlencode(','),',',$url);
257 $headers[] = $this->requestMethod." ". $url . " HTTP/1.1";
258 $headers[] = "Authorization: Basic ".base64_encode($this->user .":". $this->pass );
259 $headers[] = "Host: ".$this->server .":".$this->port;
261 if ( !isset($this->headers['content-type']) ) $this->headers['content-type'] = "Content-type: text/plain";
262 foreach( $this->headers as $ii => $head ) {
263 $headers[] = $head;
265 $headers[] = "Content-Length: " . strlen($this->body);
266 $headers[] = "User-Agent: " . $this->user_agent;
267 $headers[] = 'Connection: close';
268 $this->httpRequest = join("\r\n",$headers);
269 $this->xmlRequest = $this->body;
271 $this->httpResponse = '';
272 $this->xmlResponse = '';
274 $fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT); //error handling?
275 if ( !(get_resource_type($fip) == 'stream') ) return false;
276 if ( !fwrite($fip, $this->httpRequest."\r\n\r\n".$this->body) ) { fclose($fip); return false; }
277 $response = "";
278 while( !feof($fip) ) { $response .= fgets($fip,8192); }
279 fclose($fip);
281 list( $this->httpResponseHeaders, $this->httpResponseBody ) = preg_split( '{\r?\n\r?\n}s', $response, 2 );
282 if ( preg_match( '{Transfer-Encoding: chunked}i', $this->httpResponseHeaders ) ) $this->Unchunk();
284 $this->headers = array(); // reset the headers array for our next request
285 $this->ParseResponse($this->httpResponseBody);
286 return $response;
291 * Unchunk a chunked response
293 function Unchunk() {
294 $content = '';
295 $chunks = $this->httpResponseBody;
296 // printf( "\n================================\n%s\n================================\n", $chunks );
297 do {
298 $bytes = 0;
299 if ( preg_match('{^((\r\n)?\s*([ 0-9a-fA-F]+)(;[^\n]*)?\r?\n)}', $chunks, $matches ) ) {
300 $octets = $matches[3];
301 $bytes = hexdec($octets);
302 $pos = strlen($matches[1]);
303 // printf( "Chunk size 0x%s (%d)\n", $octets, $bytes );
304 if ( $bytes > 0 ) {
305 // printf( "---------------------------------\n%s\n---------------------------------\n", substr($chunks,$pos,$bytes) );
306 $content .= substr($chunks,$pos,$bytes);
307 $chunks = substr($chunks,$pos + $bytes + 2);
308 // printf( "+++++++++++++++++++++++++++++++++\n%s\n+++++++++++++++++++++++++++++++++\n", $chunks );
311 else {
312 $content .= $chunks;
315 while( $bytes > 0 );
316 $this->httpResponseBody = $content;
317 // printf( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n%s\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", $content );
322 * Send an OPTIONS request to the server
324 * @param string $url The URL to make the request to
326 * @return array The allowed options
328 function DoOptionsRequest( $url = null ) {
329 $this->requestMethod = "OPTIONS";
330 $this->body = "";
331 $headers = $this->DoRequest($url);
332 $options_header = preg_replace( '/^.*Allow: ([a-z, ]+)\r?\n.*/is', '$1', $headers );
333 $options = array_flip( preg_split( '/[, ]+/', $options_header ));
334 return $options;
340 * Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR)
342 * @param string $method The method (PROPFIND, REPORT, etc) to use with the request
343 * @param string $xml The XML to send along with the request
344 * @param string $url The URL to make the request to
346 * @return array An array of the allowed methods
348 function DoXMLRequest( $request_method, $xml, $url = null ) {
349 $this->body = $xml;
350 $this->requestMethod = $request_method;
351 $this->SetContentType("text/xml");
352 return $this->DoRequest($url);
358 * Get a single item from the server.
360 * @param string $url The URL to GET
362 function DoGETRequest( $url ) {
363 $this->body = "";
364 $this->requestMethod = "GET";
365 return $this->DoRequest( $url );
370 * Get the HEAD of a single item from the server.
372 * @param string $url The URL to HEAD
374 function DoHEADRequest( $url ) {
375 $this->body = "";
376 $this->requestMethod = "HEAD";
377 return $this->DoRequest( $url );
382 * PUT a text/icalendar resource, returning the etag
384 * @param string $url The URL to make the request to
385 * @param string $icalendar The iCalendar resource to send to the server
386 * @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource.
388 * @return string The content of the response from the server
390 function DoPUTRequest( $url, $icalendar, $etag = null ) {
391 $this->body = $icalendar;
393 $this->requestMethod = "PUT";
394 if ( $etag != null ) {
395 $this->SetMatch( ($etag != '*'), $etag );
397 $this->SetContentType('text/calendar; encoding="utf-8"');
398 $this->DoRequest($url);
400 $etag = null;
401 if ( preg_match( '{^ETag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1];
402 if ( !isset($etag) || $etag == '' ) {
403 printf( "No etag in:\n%s\n", $this->httpResponseHeaders );
404 $save_request = $this->httpRequest;
405 $save_response_headers = $this->httpResponseHeaders;
406 $this->DoHEADRequest( $url );
407 if ( preg_match( '{^Etag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1];
408 if ( !isset($etag) || $etag == '' ) {
409 printf( "Still No etag in:\n%s\n", $this->httpResponseHeaders );
411 $this->httpRequest = $save_request;
412 $this->httpResponseHeaders = $save_response_headers;
414 return $etag;
419 * DELETE a text/icalendar resource
421 * @param string $url The URL to make the request to
422 * @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL.
424 * @return int The HTTP Result Code for the DELETE
426 function DoDELETERequest( $url, $etag = null ) {
427 $this->body = "";
429 $this->requestMethod = "DELETE";
430 if ( $etag != null ) {
431 $this->SetMatch( true, $etag );
433 $this->DoRequest($url);
434 return $this->resultcode;
439 * Get a single item from the server.
441 * @param string $url The URL to PROPFIND on
443 function DoPROPFINDRequest( $url, $props, $depth = 0 ) {
444 $this->SetDepth($depth);
445 $xml = new XMLDocument( array( 'DAV:' => '', 'urn:ietf:params:xml:ns:caldav' => 'C' ) );
446 $prop = new XMLElement('prop');
447 foreach( $props AS $v ) {
448 $xml->NSElement($prop,$v);
451 $this->body = $xml->Render('propfind',$prop );
453 $this->requestMethod = "PROPFIND";
454 $this->SetContentType("text/xml");
455 $this->DoRequest($url);
456 return $this->GetXmlResponse();
461 * Get/Set the Principal URL
463 * @param $url string The Principal URL to set
465 function PrincipalURL( $url = null ) {
466 if ( isset($url) ) {
467 $this->principal_url = $url;
469 return $this->principal_url;
474 * Get/Set the calendar-home-set URL
476 * @param $url array of string The calendar-home-set URLs to set
478 function CalendarHomeSet( $urls = null ) {
479 if ( isset($urls) ) {
480 if ( ! is_array($urls) ) $urls = array($urls);
481 $this->calendar_home_set = $urls;
483 return $this->calendar_home_set;
488 * Get/Set the calendar-home-set URL
490 * @param $urls array of string The calendar URLs to set
492 function CalendarUrls( $urls = null ) {
493 if ( isset($urls) ) {
494 if ( ! is_array($urls) ) $urls = array($urls);
495 $this->calendar_urls = $urls;
497 return $this->calendar_urls;
502 * Return the first occurrence of an href inside the named tag.
504 * @param string $tagname The tag name to find the href inside of
506 function HrefValueInside( $tagname ) {
507 foreach( $this->xmltags[$tagname] AS $k => $v ) {
508 $j = $v + 1;
509 if ( $this->xmlnodes[$j]['tag'] == 'DAV::href' ) {
510 return rawurldecode($this->xmlnodes[$j]['value']);
513 return null;
518 * Return the href containing this property. Except only if it's inside a status != 200
520 * @param string $tagname The tag name of the property to find the href for
521 * @param integer $which Which instance of the tag should we use
523 function HrefForProp( $tagname, $i = 0 ) {
524 if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
525 $j = $this->xmltags[$tagname][$i];
526 while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' ) {
527 // printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
528 if ( $this->xmlnodes[$j]['tag'] == 'DAV::status' && $this->xmlnodes[$j]['value'] != 'HTTP/1.1 200 OK' ) return null;
530 // printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
531 if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
532 // printf( "Value[$j]: %s\n", $this->xmlnodes[$j]['value']);
533 return rawurldecode($this->xmlnodes[$j]['value']);
536 else {
537 printf( "xmltags[$tagname] or xmltags[$tagname][$i] is not set\n");
539 return null;
544 * Return the href which has a resourcetype of the specified type
546 * @param string $tagname The tag name of the resourcetype to find the href for
547 * @param integer $which Which instance of the tag should we use
549 function HrefForResourcetype( $tagname, $i = 0 ) {
550 if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
551 $j = $this->xmltags[$tagname][$i];
552 while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::resourcetype' );
553 if ( $j > 0 ) {
554 while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' );
555 if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
556 return rawurldecode($this->xmlnodes[$j]['value']);
560 return null;
565 * Return the <prop> ... </prop> of a propstat where the status is OK
567 * @param string $nodenum The node number in the xmlnodes which is the href
569 function GetOKProps( $nodenum ) {
570 $props = null;
571 $level = $this->xmlnodes[$nodenum]['level'];
572 $status = '';
573 while ( $this->xmlnodes[++$nodenum]['level'] >= $level ) {
574 if ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::propstat' ) {
575 if ( $this->xmlnodes[$nodenum]['type'] == 'open' ) {
576 $props = array();
577 $status = '';
579 else {
580 if ( $status == 'HTTP/1.1 200 OK' ) break;
583 elseif ( !isset($this->xmlnodes[$nodenum]) || !is_array($this->xmlnodes[$nodenum]) ) {
584 break;
586 elseif ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::status' ) {
587 $status = $this->xmlnodes[$nodenum]['value'];
589 else {
590 $props[] = $this->xmlnodes[$nodenum];
593 return $props;
598 * Attack the given URL in an attempt to find a principal URL
600 * @param string $url The URL to find the principal-URL from
602 function FindPrincipal( $url=null ) {
603 $xml = $this->DoPROPFINDRequest( $url, array('resourcetype', 'current-user-principal', 'owner', 'principal-URL',
604 'urn:ietf:params:xml:ns:caldav:calendar-home-set'), 1);
606 $principal_url = $this->HrefForProp('DAV::principal');
608 if ( !isset($principal_url) ) {
609 foreach( array('DAV::current-user-principal', 'DAV::principal-URL', 'DAV::owner') AS $href ) {
610 if ( !isset($principal_url) ) {
611 $principal_url = $this->HrefValueInside($href);
616 return $this->PrincipalURL($principal_url);
621 * Attack the given URL in an attempt to find a principal URL
623 * @param string $url The URL to find the calendar-home-set from
625 function FindCalendarHome( $recursed=false ) {
626 if ( !isset($this->principal_url) ) {
627 $this->FindPrincipal();
629 if ( $recursed ) {
630 $this->DoPROPFINDRequest( $this->principal_url, array('urn:ietf:params:xml:ns:caldav:calendar-home-set'), 0);
633 $calendar_home = array();
634 foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-home-set'] AS $k => $v ) {
635 if ( $this->xmlnodes[$v]['type'] != 'open' ) continue;
636 while( $this->xmlnodes[++$v]['type'] != 'close' && $this->xmlnodes[$v]['tag'] != 'urn:ietf:params:xml:ns:caldav:calendar-home-set' ) {
637 // printf( "Tag: '%s' = '%s'\n", $this->xmlnodes[$v]['tag'], $this->xmlnodes[$v]['value']);
638 if ( $this->xmlnodes[$v]['tag'] == 'DAV::href' && isset($this->xmlnodes[$v]['value']) )
639 $calendar_home[] = rawurldecode($this->xmlnodes[$v]['value']);
643 if ( !$recursed && count($calendar_home) < 1 ) {
644 $calendar_home = $this->FindCalendarHome(true);
647 return $this->CalendarHomeSet($calendar_home);
652 * Find the calendars, from the calendar_home_set
654 function FindCalendars( $recursed=false ) {
655 if ( !isset($this->calendar_home_set[0]) ) {
656 $this->FindCalendarHome();
658 $this->DoPROPFINDRequest( $this->calendar_home_set[0], array('resourcetype','displayname','http://calendarserver.org/ns/:getctag'), 1);
660 $calendars = array();
661 if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar']) ) {
662 $calendar_urls = array();
663 foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar'] AS $k => $v ) {
664 $calendar_urls[$this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar', $k)] = 1;
667 foreach( $this->xmltags['DAV::href'] AS $i => $hnode ) {
668 $href = rawurldecode($this->xmlnodes[$hnode]['value']);
670 if ( !isset($calendar_urls[$href]) ) continue;
672 // printf("Seems '%s' is a calendar.\n", $href );
674 $calendar = new CalendarInfo($href);
675 $ok_props = $this->GetOKProps($hnode);
676 foreach( $ok_props AS $v ) {
677 // printf("Looking at: %s[%s]\n", $href, $v['tag'] );
678 switch( $v['tag'] ) {
679 case 'http://calendarserver.org/ns/:getctag':
680 $calendar->getctag = $v['value'];
681 break;
682 case 'DAV::displayname':
683 $calendar->displayname = $v['value'];
684 break;
687 $calendars[] = $calendar;
691 return $this->CalendarUrls($calendars);
696 * Find the calendars, from the calendar_home_set
698 function GetCalendarDetails( $url = null ) {
699 if ( isset($url) ) $this->SetCalendar($url);
701 $calendar_properties = array( 'resourcetype', 'displayname', 'http://calendarserver.org/ns/:getctag', 'urn:ietf:params:xml:ns:caldav:calendar-timezone', 'supported-report-set' );
702 $this->DoPROPFINDRequest( $this->calendar_url, $calendar_properties, 0);
704 $hnode = $this->xmltags['DAV::href'][0];
705 $href = rawurldecode($this->xmlnodes[$hnode]['value']);
707 $calendar = new CalendarInfo($href);
708 $ok_props = $this->GetOKProps($hnode);
709 foreach( $ok_props AS $k => $v ) {
710 $name = preg_replace( '{^.*:}', '', $v['tag'] );
711 if ( isset($v['value'] ) ) {
712 $calendar->{$name} = $v['value'];
714 /* else {
715 printf( "Calendar property '%s' has no text content\n", $v['tag'] );
719 return $calendar;
724 * Get all etags for a calendar
726 function GetCollectionETags( $url = null ) {
727 if ( isset($url) ) $this->SetCalendar($url);
729 $this->DoPROPFINDRequest( $this->calendar_url, array('getetag'), 1);
731 $etags = array();
732 if ( isset($this->xmltags['DAV::getetag']) ) {
733 foreach( $this->xmltags['DAV::getetag'] AS $k => $v ) {
734 $href = $this->HrefForProp('DAV::getetag', $k);
735 if ( isset($href) && isset($this->xmlnodes[$v]['value']) ) $etags[$href] = $this->xmlnodes[$v]['value'];
739 return $etags;
744 * Get a bunch of events for a calendar with a calendar-multiget report
746 function CalendarMultiget( $event_hrefs, $url = null ) {
748 if ( isset($url) ) $this->SetCalendar($url);
750 $hrefs = '';
751 foreach( $event_hrefs AS $k => $href ) {
752 $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
753 $hrefs .= '<href>'.$href.'</href>';
755 $this->body = <<<EOXML
756 <?xml version="1.0" encoding="utf-8" ?>
757 <C:calendar-multiget xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
758 <prop><getetag/><C:calendar-data/></prop>
759 $hrefs
760 </C:calendar-multiget>
761 EOXML;
763 $this->requestMethod = "REPORT";
764 $this->SetContentType("text/xml");
765 $this->DoRequest( $this->calendar_url );
767 $events = array();
768 if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data']) ) {
769 foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data'] AS $k => $v ) {
770 $href = $this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar-data', $k);
771 // echo "Calendar-data:\n"; print_r($this->xmlnodes[$v]);
772 $events[$href] = $this->xmlnodes[$v]['value'];
775 else {
776 foreach( $event_hrefs AS $k => $href ) {
777 $this->DoGETRequest($href);
778 $events[$href] = $this->httpResponseBody;
782 return $events;
787 * Given XML for a calendar query, return an array of the events (/todos) in the
788 * response. Each event in the array will have a 'href', 'etag' and '$response_type'
789 * part, where the 'href' is relative to the calendar and the '$response_type' contains the
790 * definition of the calendar data in iCalendar format.
792 * @param string $filter XML fragment which is the <filter> element of a calendar-query
793 * @param string $url The URL of the calendar, or empty/null to use the 'current' calendar_url
795 * @return array An array of the relative URLs, etags, and events from the server. Each element of the array will
796 * be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied
797 * etag (which only varies when the data changes) and the calendar data in iCalendar format.
799 function DoCalendarQuery( $filter, $url = '' ) {
801 if ( !empty($url) ) $this->SetCalendar($url);
803 $this->body = <<<EOXML
804 <?xml version="1.0" encoding="utf-8" ?>
805 <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
806 <D:prop>
807 <C:calendar-data/>
808 <D:getetag/>
809 </D:prop>$filter
810 </C:calendar-query>
811 EOXML;
813 $this->requestMethod = "REPORT";
814 $this->SetContentType("text/xml");
815 $this->DoRequest( $this->calendar_url );
817 $report = array();
818 foreach( $this->xmlnodes as $k => $v ) {
819 switch( $v['tag'] ) {
820 case 'DAV::response':
821 if ( $v['type'] == 'open' ) {
822 $response = array();
824 elseif ( $v['type'] == 'close' ) {
825 $report[] = $response;
827 break;
828 case 'DAV::href':
829 $response['href'] = basename( rawurldecode($v['value']) );
830 break;
831 case 'DAV::getetag':
832 $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
833 break;
834 case 'urn:ietf:params:xml:ns:caldav:calendar-data':
835 $response['data'] = $v['value'];
836 break;
839 return $report;
844 * Get the events in a range from $start to $finish. The dates should be in the
845 * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
846 * array of event arrays. Each event array will have a 'href', 'etag' and 'event'
847 * part, where the 'href' is relative to the calendar and the event contains the
848 * definition of the event in iCalendar format.
850 * @param timestamp $start The start time for the period
851 * @param timestamp $finish The finish time for the period
852 * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
854 * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
856 function GetEvents( $start = null, $finish = null, $relative_url = '' ) {
857 $filter = "";
858 if ( isset($start) && isset($finish) )
859 $range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
860 else
861 $range = '';
863 $filter = <<<EOFILTER
864 <C:filter>
865 <C:comp-filter name="VCALENDAR">
866 <C:comp-filter name="VEVENT">
867 $range
868 </C:comp-filter>
869 </C:comp-filter>
870 </C:filter>
871 EOFILTER;
873 return $this->DoCalendarQuery($filter, $relative_url);
878 * Get the todo's in a range from $start to $finish. The dates should be in the
879 * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
880 * array of event arrays. Each event array will have a 'href', 'etag' and 'event'
881 * part, where the 'href' is relative to the calendar and the event contains the
882 * definition of the event in iCalendar format.
884 * @param timestamp $start The start time for the period
885 * @param timestamp $finish The finish time for the period
886 * @param boolean $completed Whether to include completed tasks
887 * @param boolean $cancelled Whether to include cancelled tasks
888 * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
890 * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
892 function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = "" ) {
894 if ( $start && $finish ) {
895 $time_range = <<<EOTIME
896 <C:time-range start="$start" end="$finish"/>
897 EOTIME;
900 // Warning! May contain traces of double negatives...
901 $neg_cancelled = ( $cancelled === true ? "no" : "yes" );
902 $neg_completed = ( $cancelled === true ? "no" : "yes" );
904 $filter = <<<EOFILTER
905 <C:filter>
906 <C:comp-filter name="VCALENDAR">
907 <C:comp-filter name="VTODO">
908 <C:prop-filter name="STATUS">
909 <C:text-match negate-condition="$neg_completed">COMPLETED</C:text-match>
910 </C:prop-filter>
911 <C:prop-filter name="STATUS">
912 <C:text-match negate-condition="$neg_cancelled">CANCELLED</C:text-match>
913 </C:prop-filter>$time_range
914 </C:comp-filter>
915 </C:comp-filter>
916 </C:filter>
917 EOFILTER;
919 return $this->DoCalendarQuery($filter, $relative_url);
924 * Get the calendar entry by UID
926 * @param uid
927 * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
928 * @param string $component_type The component type inside the VCALENDAR. Default 'VEVENT'.
930 * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery()
932 function GetEntryByUid( $uid, $relative_url = '', $component_type = 'VEVENT' ) {
933 $filter = "";
934 if ( $uid ) {
935 $filter = <<<EOFILTER
936 <C:filter>
937 <C:comp-filter name="VCALENDAR">
938 <C:comp-filter name="$component_type">
939 <C:prop-filter name="UID">
940 <C:text-match icollation="i;octet">$uid</C:text-match>
941 </C:prop-filter>
942 </C:comp-filter>
943 </C:comp-filter>
944 </C:filter>
945 EOFILTER;
948 return $this->DoCalendarQuery($filter, $relative_url);
953 * Get the calendar entry by HREF
955 * @param string $href The href from a call to GetEvents or GetTodos etc.
957 * @return string The iCalendar of the calendar entry
959 function GetEntryByHref( $href ) {
960 $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
961 return $this->DoGETRequest( $href );
967 * Usage example
969 * $cal = new CalDAVClient( "http://calendar.example.com/caldav.php/username/calendar/", "username", "password", "calendar" );
970 * $options = $cal->DoOptionsRequest();
971 * if ( isset($options["PROPFIND"]) ) {
972 * // Fetch some information about the events in that calendar
973 * $cal->SetDepth(1);
974 * $folder_xml = $cal->DoXMLRequest("PROPFIND", '<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><getcontentlength/><getcontenttype/><resourcetype/><getetag/></prop></propfind>' );
976 * // Fetch all events for February
977 * $events = $cal->GetEvents("20070101T000000Z","20070201T000000Z");
978 * foreach ( $events AS $k => $event ) {
979 * do_something_with_event_data( $event['data'] );
981 * $acc = array();
982 * $acc["google"] = array(
983 * "user"=>"kunsttherapie@gmail.com",
984 * "pass"=>"xxxxx",
985 * "server"=>"ssl://www.google.com",
986 * "port"=>"443",
987 * "uri"=>"https://www.google.com/calendar/dav/kunsttherapie@gmail.com/events/",
988 * );
990 * $acc["davical"] = array(
991 * "user"=>"some_user",
992 * "pass"=>"big secret",
993 * "server"=>"calendar.foo.bar",
994 * "port"=>"80",
995 * "uri"=>"http://calendar.foo.bar/caldav.php/some_user/home/",
996 * );
997 * //*******************************
999 * $account = $acc["davical"];
1001 * //*******************************
1002 * $cal = new CalDAVClient( $account["uri"], $account["user"], $account["pass"], "", $account["server"], $account["port"] );
1003 * $options = $cal->DoOptionsRequest();
1004 * print_r($options);
1006 * //*******************************
1007 * //*******************************
1009 * $xmlC = <<<PROPP
1010 * <?xml version="1.0" encoding="utf-8" ?>
1011 * <D:propfind xmlns:D="DAV:" xmlns:C="http://calendarserver.org/ns/">
1012 * <D:prop>
1013 * <D:displayname />
1014 * <C:getctag />
1015 * <D:resourcetype />
1017 * </D:prop>
1018 * </D:propfind>
1019 * PROPP;
1020 * //if ( isset($options["PROPFIND"]) ) {
1021 * // Fetch some information about the events in that calendar
1022 * // $cal->SetDepth(1);
1023 * // $folder_xml = $cal->DoXMLRequest("PROPFIND", $xmlC);
1024 * // print_r( $folder_xml);
1025 * //}
1027 * // Fetch all events for February
1028 * $events = $cal->GetEvents("20090201T000000Z","20090301T000000Z");
1029 * foreach ( $events as $k => $event ) {
1030 * print_r($event['data']);
1031 * print "\n---------------------------------------------\n";
1034 * //*******************************
1035 * //*******************************