Final fix to status code response. Add a debug flag to limit output.
[davical.git] / inc / caldav-client-v2.php
blob7ca476d44e9f8260ea3a0ae48af5174a317f8a08
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 $xmlResponse = ""; // xml received
80 protected $httpResponseCode = 0; // http response code
81 protected $httpResponseHeaders = "";
82 protected $httpResponseBody = "";
84 protected $parser; // our XML parser object
86 private $debug = false; // Whether we are debugging
88 /**
89 * Constructor, initialises the class
91 * @param string $base_url The URL for the calendar server
92 * @param string $user The name of the user logging in
93 * @param string $pass The password for that user
95 function __construct( $base_url, $user, $pass ) {
96 $this->user = $user;
97 $this->pass = $pass;
98 $this->headers = array();
100 if ( preg_match( '#^(https?)://([a-z0-9.-]+)(:([0-9]+))?(/.*)$#', $base_url, $matches ) ) {
101 $this->server = $matches[2];
102 $this->base_url = $matches[5];
103 if ( $matches[1] == 'https' ) {
104 $this->protocol = 'ssl';
105 $this->port = 443;
107 else {
108 $this->protocol = 'tcp';
109 $this->port = 80;
111 if ( $matches[4] != '' ) {
112 $this->port = intval($matches[4]);
115 else {
116 trigger_error("Invalid URL: '".$base_url."'", E_USER_ERROR);
122 * Call this to enable / disable debugging. It will return the prior value of the debugging flag.
123 * @param boolean $new_value The new value for debugging.
124 * @return boolean The previous value, in case you want to restore it later.
126 function SetDebug( $new_value ) {
127 $old_value = $this->debug;
128 if ( $new_value )
129 $this->debug = true;
130 else
131 $this->debug = false;
132 return $old_value;
138 * Adds an If-Match or If-None-Match header
140 * @param bool $match to Match or Not to Match, that is the question!
141 * @param string $etag The etag to match / not match against.
143 function SetMatch( $match, $etag = '*' ) {
144 $this->headers['match'] = sprintf( "%s-Match: \"%s\"", ($match ? "If" : "If-None"), trim($etag,'"'));
148 * Add a Depth: header. Valid values are 0, 1 or infinity
150 * @param int $depth The depth, default to infinity
152 function SetDepth( $depth = '0' ) {
153 $this->headers['depth'] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") );
157 * Add a Depth: header. Valid values are 1 or infinity
159 * @param int $depth The depth, default to infinity
161 function SetUserAgent( $user_agent = null ) {
162 if ( !isset($user_agent) ) $user_agent = $this->user_agent;
163 $this->user_agent = $user_agent;
167 * Add a Content-type: header.
169 * @param string $type The content type
171 function SetContentType( $type ) {
172 $this->headers['content-type'] = "Content-type: $type";
176 * Set the calendar_url we will be using for a while.
178 * @param string $url The calendar_url
180 function SetCalendar( $url ) {
181 $this->calendar_url = $url;
185 * Split response into httpResponse and xmlResponse
187 * @param string Response from server
189 function ParseResponse( $response ) {
190 $pos = strpos($response, '<?xml');
191 if ($pos !== false) {
192 $this->xmlResponse = trim(substr($response, $pos));
193 $this->xmlResponse = preg_replace('{>[^>]*$}s', '>',$this->xmlResponse );
194 $parser = xml_parser_create_ns('UTF-8');
195 xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 );
196 xml_parser_set_option ( $parser, XML_OPTION_CASE_FOLDING, 0 );
198 if ( xml_parse_into_struct( $parser, $this->xmlResponse, $this->xmlnodes, $this->xmltags ) === 0 ) {
199 printf( "XML parsing error: %s - %s\n", xml_get_error_code($parser), xml_error_string(xml_get_error_code($parser)) );
200 // debug_print_backtrace();
201 // echo "\nNodes array............................................................\n"; print_r( $this->xmlnodes );
202 // echo "\nTags array............................................................\n"; print_r( $this->xmltags );
203 printf( "\nXML Reponse:\n%s\n", $this->xmlResponse );
206 xml_parser_free($parser);
211 * Output http request headers
213 * @return HTTP headers
215 function GetHttpRequest() {
216 return $this->httpRequest;
219 * Output http response headers
221 * @return HTTP headers
223 function GetResponseHeaders() {
224 return $this->httpResponseHeaders;
227 * Output http response body
229 * @return HTTP body
231 function GetResponseBody() {
232 return $this->httpResponseBody;
235 * Output xml request
237 * @return raw xml
239 function GetXmlRequest() {
240 return $this->xmlRequest;
243 * Output xml response
245 * @return raw xml
247 function GetXmlResponse() {
248 return $this->xmlResponse;
252 * Send a request to the server
254 * @param string $url The URL to make the request to
256 * @return string The content of the response from the server
258 function DoRequest( $url = null ) {
259 if(!defined("_FSOCK_TIMEOUT")){ define("_FSOCK_TIMEOUT", 10); }
260 $headers = array();
262 if ( !isset($url) ) $url = $this->base_url;
263 $this->request_url = $url;
264 $url = preg_replace('{^https?://[^/]+}', '', $url);
265 // URLencode if it isn't already
266 if ( preg_match( '{[^%?&=+,.-_/a-z0-9]}', $url ) ) {
267 $url = str_replace(rawurlencode('/'),'/',rawurlencode($url));
268 $url = str_replace(rawurlencode('?'),'?',$url);
269 $url = str_replace(rawurlencode('&'),'&',$url);
270 $url = str_replace(rawurlencode('='),'=',$url);
271 $url = str_replace(rawurlencode('+'),'+',$url);
272 $url = str_replace(rawurlencode(','),',',$url);
274 $headers[] = $this->requestMethod." ". $url . " HTTP/1.1";
275 $headers[] = "Authorization: Basic ".base64_encode($this->user .":". $this->pass );
276 $headers[] = "Host: ".$this->server .":".$this->port;
278 if ( !isset($this->headers['content-type']) ) $this->headers['content-type'] = "Content-type: text/plain";
279 foreach( $this->headers as $ii => $head ) {
280 $headers[] = $head;
282 $headers[] = "Content-Length: " . strlen($this->body);
283 $headers[] = "User-Agent: " . $this->user_agent;
284 $headers[] = 'Connection: close';
285 $this->httpRequest = join("\r\n",$headers);
286 $this->xmlRequest = $this->body;
288 $this->xmlResponse = '';
290 $fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT); //error handling?
291 if ( !(get_resource_type($fip) == 'stream') ) return false;
292 if ( !fwrite($fip, $this->httpRequest."\r\n\r\n".$this->body) ) { fclose($fip); return false; }
293 $response = "";
294 while( !feof($fip) ) { $response .= fgets($fip,8192); }
295 fclose($fip);
297 list( $this->httpResponseHeaders, $this->httpResponseBody ) = preg_split( '{\r?\n\r?\n}s', $response, 2 );
298 if ( preg_match( '{Transfer-Encoding: chunked}i', $this->httpResponseHeaders ) ) $this->Unchunk();
299 if ( preg_match('/HTTP\/\d\.\d (\d{3})/', $this->httpResponseHeaders, $status) )
300 $this->httpResponseCode = intval($status[1]);
301 else
302 $this->httpResponseCode = 0;
304 $this->headers = array(); // reset the headers array for our next request
305 $this->ParseResponse($this->httpResponseBody);
306 return $response;
311 * Unchunk a chunked response
313 function Unchunk() {
314 $content = '';
315 $chunks = $this->httpResponseBody;
316 // printf( "\n================================\n%s\n================================\n", $chunks );
317 do {
318 $bytes = 0;
319 if ( preg_match('{^((\r\n)?\s*([ 0-9a-fA-F]+)(;[^\n]*)?\r?\n)}', $chunks, $matches ) ) {
320 $octets = $matches[3];
321 $bytes = hexdec($octets);
322 $pos = strlen($matches[1]);
323 // printf( "Chunk size 0x%s (%d)\n", $octets, $bytes );
324 if ( $bytes > 0 ) {
325 // printf( "---------------------------------\n%s\n---------------------------------\n", substr($chunks,$pos,$bytes) );
326 $content .= substr($chunks,$pos,$bytes);
327 $chunks = substr($chunks,$pos + $bytes + 2);
328 // printf( "+++++++++++++++++++++++++++++++++\n%s\n+++++++++++++++++++++++++++++++++\n", $chunks );
331 else {
332 $content .= $chunks;
335 while( $bytes > 0 );
336 $this->httpResponseBody = $content;
337 // printf( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n%s\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", $content );
342 * Send an OPTIONS request to the server
344 * @param string $url The URL to make the request to
346 * @return array The allowed options
348 function DoOptionsRequest( $url = null ) {
349 $this->requestMethod = "OPTIONS";
350 $this->body = "";
351 $headers = $this->DoRequest($url);
352 $options_header = preg_replace( '/^.*Allow: ([a-z, ]+)\r?\n.*/is', '$1', $headers );
353 $options = array_flip( preg_split( '/[, ]+/', $options_header ));
354 return $options;
360 * Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR)
362 * @param string $method The method (PROPFIND, REPORT, etc) to use with the request
363 * @param string $xml The XML to send along with the request
364 * @param string $url The URL to make the request to
366 * @return array An array of the allowed methods
368 function DoXMLRequest( $request_method, $xml, $url = null ) {
369 $this->body = $xml;
370 $this->requestMethod = $request_method;
371 $this->SetContentType("text/xml");
372 return $this->DoRequest($url);
378 * Get a single item from the server.
380 * @param string $url The URL to GET
382 function DoGETRequest( $url ) {
383 $this->body = "";
384 $this->requestMethod = "GET";
385 return $this->DoRequest( $url );
390 * Get the HEAD of a single item from the server.
392 * @param string $url The URL to HEAD
394 function DoHEADRequest( $url ) {
395 $this->body = "";
396 $this->requestMethod = "HEAD";
397 return $this->DoRequest( $url );
402 * PUT a text/icalendar resource, returning the etag
404 * @param string $url The URL to make the request to
405 * @param string $icalendar The iCalendar resource to send to the server
406 * @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource.
408 * @return string The content of the response from the server
410 function DoPUTRequest( $url, $icalendar, $etag = null ) {
411 $this->body = $icalendar;
413 $this->requestMethod = "PUT";
414 if ( $etag != null ) {
415 $this->SetMatch( ($etag != '*'), $etag );
417 $this->SetContentType('text/calendar; encoding="utf-8"');
418 $this->DoRequest($url);
420 $etag = null;
421 if ( preg_match( '{^ETag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1];
422 if ( !isset($etag) || $etag == '' ) {
423 if ( $this->debug ) printf( "No etag in:\n%s\n", $this->httpResponseHeaders );
424 $save_request = $this->httpRequest;
425 $save_response_headers = $this->httpResponseHeaders;
426 $this->DoHEADRequest( $url );
427 if ( preg_match( '{^Etag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1];
428 if ( !isset($etag) || $etag == '' ) {
429 if ( $this->debug ) printf( "Still No etag in:\n%s\n", $this->httpResponseHeaders );
431 $this->httpRequest = $save_request;
432 $this->httpResponseHeaders = $save_response_headers;
434 return $etag;
439 * DELETE a text/icalendar resource
441 * @param string $url The URL to make the request to
442 * @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL.
444 * @return int The HTTP Result Code for the DELETE
446 function DoDELETERequest( $url, $etag = null ) {
447 $this->body = "";
449 $this->requestMethod = "DELETE";
450 if ( $etag != null ) {
451 $this->SetMatch( true, $etag );
453 $this->DoRequest($url);
454 return $this->httpResponseCode;
459 * Get a single item from the server.
461 * @param string $url The URL to PROPFIND on
463 function DoPROPFINDRequest( $url, $props, $depth = 0 ) {
464 $this->SetDepth($depth);
465 $xml = new XMLDocument( array( 'DAV:' => '', 'urn:ietf:params:xml:ns:caldav' => 'C' ) );
466 $prop = new XMLElement('prop');
467 foreach( $props AS $v ) {
468 $xml->NSElement($prop,$v);
471 $this->body = $xml->Render('propfind',$prop );
473 $this->requestMethod = "PROPFIND";
474 $this->SetContentType("text/xml");
475 $this->DoRequest($url);
476 return $this->GetXmlResponse();
481 * Get/Set the Principal URL
483 * @param $url string The Principal URL to set
485 function PrincipalURL( $url = null ) {
486 if ( isset($url) ) {
487 $this->principal_url = $url;
489 return $this->principal_url;
494 * Get/Set the calendar-home-set URL
496 * @param $url array of string The calendar-home-set URLs to set
498 function CalendarHomeSet( $urls = null ) {
499 if ( isset($urls) ) {
500 if ( ! is_array($urls) ) $urls = array($urls);
501 $this->calendar_home_set = $urls;
503 return $this->calendar_home_set;
508 * Get/Set the calendar-home-set URL
510 * @param $urls array of string The calendar URLs to set
512 function CalendarUrls( $urls = null ) {
513 if ( isset($urls) ) {
514 if ( ! is_array($urls) ) $urls = array($urls);
515 $this->calendar_urls = $urls;
517 return $this->calendar_urls;
522 * Return the first occurrence of an href inside the named tag.
524 * @param string $tagname The tag name to find the href inside of
526 function HrefValueInside( $tagname ) {
527 foreach( $this->xmltags[$tagname] AS $k => $v ) {
528 $j = $v + 1;
529 if ( $this->xmlnodes[$j]['tag'] == 'DAV::href' ) {
530 return rawurldecode($this->xmlnodes[$j]['value']);
533 return null;
538 * Return the href containing this property. Except only if it's inside a status != 200
540 * @param string $tagname The tag name of the property to find the href for
541 * @param integer $which Which instance of the tag should we use
543 function HrefForProp( $tagname, $i = 0 ) {
544 if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
545 $j = $this->xmltags[$tagname][$i];
546 while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' ) {
547 // printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
548 if ( $this->xmlnodes[$j]['tag'] == 'DAV::status' && $this->xmlnodes[$j]['value'] != 'HTTP/1.1 200 OK' ) return null;
550 // printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
551 if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
552 // printf( "Value[$j]: %s\n", $this->xmlnodes[$j]['value']);
553 return rawurldecode($this->xmlnodes[$j]['value']);
556 else {
557 if ( $this->debug ) printf( "xmltags[$tagname] or xmltags[$tagname][$i] is not set\n");
559 return null;
564 * Return the href which has a resourcetype of the specified type
566 * @param string $tagname The tag name of the resourcetype to find the href for
567 * @param integer $which Which instance of the tag should we use
569 function HrefForResourcetype( $tagname, $i = 0 ) {
570 if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
571 $j = $this->xmltags[$tagname][$i];
572 while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::resourcetype' );
573 if ( $j > 0 ) {
574 while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' );
575 if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
576 return rawurldecode($this->xmlnodes[$j]['value']);
580 return null;
585 * Return the <prop> ... </prop> of a propstat where the status is OK
587 * @param string $nodenum The node number in the xmlnodes which is the href
589 function GetOKProps( $nodenum ) {
590 $props = null;
591 $level = $this->xmlnodes[$nodenum]['level'];
592 $status = '';
593 while ( $this->xmlnodes[++$nodenum]['level'] >= $level ) {
594 if ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::propstat' ) {
595 if ( $this->xmlnodes[$nodenum]['type'] == 'open' ) {
596 $props = array();
597 $status = '';
599 else {
600 if ( $status == 'HTTP/1.1 200 OK' ) break;
603 elseif ( !isset($this->xmlnodes[$nodenum]) || !is_array($this->xmlnodes[$nodenum]) ) {
604 break;
606 elseif ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::status' ) {
607 $status = $this->xmlnodes[$nodenum]['value'];
609 else {
610 $props[] = $this->xmlnodes[$nodenum];
613 return $props;
618 * Attack the given URL in an attempt to find a principal URL
620 * @param string $url The URL to find the principal-URL from
622 function FindPrincipal( $url=null ) {
623 $xml = $this->DoPROPFINDRequest( $url, array('resourcetype', 'current-user-principal', 'owner', 'principal-URL',
624 'urn:ietf:params:xml:ns:caldav:calendar-home-set'), 1);
626 $principal_url = $this->HrefForProp('DAV::principal');
628 if ( !isset($principal_url) ) {
629 foreach( array('DAV::current-user-principal', 'DAV::principal-URL', 'DAV::owner') AS $href ) {
630 if ( !isset($principal_url) ) {
631 $principal_url = $this->HrefValueInside($href);
636 return $this->PrincipalURL($principal_url);
641 * Attack the given URL in an attempt to find a principal URL
643 * @param string $url The URL to find the calendar-home-set from
645 function FindCalendarHome( $recursed=false ) {
646 if ( !isset($this->principal_url) ) {
647 $this->FindPrincipal();
649 if ( $recursed ) {
650 $this->DoPROPFINDRequest( $this->principal_url, array('urn:ietf:params:xml:ns:caldav:calendar-home-set'), 0);
653 $calendar_home = array();
654 foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-home-set'] AS $k => $v ) {
655 if ( $this->xmlnodes[$v]['type'] != 'open' ) continue;
656 while( $this->xmlnodes[++$v]['type'] != 'close' && $this->xmlnodes[$v]['tag'] != 'urn:ietf:params:xml:ns:caldav:calendar-home-set' ) {
657 // printf( "Tag: '%s' = '%s'\n", $this->xmlnodes[$v]['tag'], $this->xmlnodes[$v]['value']);
658 if ( $this->xmlnodes[$v]['tag'] == 'DAV::href' && isset($this->xmlnodes[$v]['value']) )
659 $calendar_home[] = rawurldecode($this->xmlnodes[$v]['value']);
663 if ( !$recursed && count($calendar_home) < 1 ) {
664 $calendar_home = $this->FindCalendarHome(true);
667 return $this->CalendarHomeSet($calendar_home);
672 * Find the calendars, from the calendar_home_set
674 function FindCalendars( $recursed=false ) {
675 if ( !isset($this->calendar_home_set[0]) ) {
676 $this->FindCalendarHome();
678 $this->DoPROPFINDRequest( $this->calendar_home_set[0], array('resourcetype','displayname','http://calendarserver.org/ns/:getctag'), 1);
680 $calendars = array();
681 if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar']) ) {
682 $calendar_urls = array();
683 foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar'] AS $k => $v ) {
684 $calendar_urls[$this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar', $k)] = 1;
687 foreach( $this->xmltags['DAV::href'] AS $i => $hnode ) {
688 $href = rawurldecode($this->xmlnodes[$hnode]['value']);
690 if ( !isset($calendar_urls[$href]) ) continue;
692 // printf("Seems '%s' is a calendar.\n", $href );
694 $calendar = new CalendarInfo($href);
695 $ok_props = $this->GetOKProps($hnode);
696 foreach( $ok_props AS $v ) {
697 // printf("Looking at: %s[%s]\n", $href, $v['tag'] );
698 switch( $v['tag'] ) {
699 case 'http://calendarserver.org/ns/:getctag':
700 $calendar->getctag = $v['value'];
701 break;
702 case 'DAV::displayname':
703 $calendar->displayname = $v['value'];
704 break;
707 $calendars[] = $calendar;
711 return $this->CalendarUrls($calendars);
716 * Find the calendars, from the calendar_home_set
718 function GetCalendarDetails( $url = null ) {
719 if ( isset($url) ) $this->SetCalendar($url);
721 $calendar_properties = array( 'resourcetype', 'displayname', 'http://calendarserver.org/ns/:getctag', 'urn:ietf:params:xml:ns:caldav:calendar-timezone', 'supported-report-set' );
722 $this->DoPROPFINDRequest( $this->calendar_url, $calendar_properties, 0);
724 $hnode = $this->xmltags['DAV::href'][0];
725 $href = rawurldecode($this->xmlnodes[$hnode]['value']);
727 $calendar = new CalendarInfo($href);
728 $ok_props = $this->GetOKProps($hnode);
729 foreach( $ok_props AS $k => $v ) {
730 $name = preg_replace( '{^.*:}', '', $v['tag'] );
731 if ( isset($v['value'] ) ) {
732 $calendar->{$name} = $v['value'];
734 /* else {
735 printf( "Calendar property '%s' has no text content\n", $v['tag'] );
739 return $calendar;
744 * Get all etags for a calendar
746 function GetCollectionETags( $url = null ) {
747 if ( isset($url) ) $this->SetCalendar($url);
749 $this->DoPROPFINDRequest( $this->calendar_url, array('getetag'), 1);
751 $etags = array();
752 if ( isset($this->xmltags['DAV::getetag']) ) {
753 foreach( $this->xmltags['DAV::getetag'] AS $k => $v ) {
754 $href = $this->HrefForProp('DAV::getetag', $k);
755 if ( isset($href) && isset($this->xmlnodes[$v]['value']) ) $etags[$href] = $this->xmlnodes[$v]['value'];
759 return $etags;
764 * Get a bunch of events for a calendar with a calendar-multiget report
766 function CalendarMultiget( $event_hrefs, $url = null ) {
768 if ( isset($url) ) $this->SetCalendar($url);
770 $hrefs = '';
771 foreach( $event_hrefs AS $k => $href ) {
772 $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
773 $hrefs .= '<href>'.$href.'</href>';
775 $this->body = <<<EOXML
776 <?xml version="1.0" encoding="utf-8" ?>
777 <C:calendar-multiget xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
778 <prop><getetag/><C:calendar-data/></prop>
779 $hrefs
780 </C:calendar-multiget>
781 EOXML;
783 $this->requestMethod = "REPORT";
784 $this->SetContentType("text/xml");
785 $this->DoRequest( $this->calendar_url );
787 $events = array();
788 if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data']) ) {
789 foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data'] AS $k => $v ) {
790 $href = $this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar-data', $k);
791 // echo "Calendar-data:\n"; print_r($this->xmlnodes[$v]);
792 $events[$href] = $this->xmlnodes[$v]['value'];
795 else {
796 foreach( $event_hrefs AS $k => $href ) {
797 $this->DoGETRequest($href);
798 $events[$href] = $this->httpResponseBody;
802 return $events;
807 * Given XML for a calendar query, return an array of the events (/todos) in the
808 * response. Each event in the array will have a 'href', 'etag' and '$response_type'
809 * part, where the 'href' is relative to the calendar and the '$response_type' contains the
810 * definition of the calendar data in iCalendar format.
812 * @param string $filter XML fragment which is the <filter> element of a calendar-query
813 * @param string $url The URL of the calendar, or empty/null to use the 'current' calendar_url
815 * @return array An array of the relative URLs, etags, and events from the server. Each element of the array will
816 * be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied
817 * etag (which only varies when the data changes) and the calendar data in iCalendar format.
819 function DoCalendarQuery( $filter, $url = '' ) {
821 if ( !empty($url) ) $this->SetCalendar($url);
823 $this->body = <<<EOXML
824 <?xml version="1.0" encoding="utf-8" ?>
825 <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
826 <D:prop>
827 <C:calendar-data/>
828 <D:getetag/>
829 </D:prop>$filter
830 </C:calendar-query>
831 EOXML;
833 $this->requestMethod = "REPORT";
834 $this->SetContentType("text/xml");
835 $this->DoRequest( $this->calendar_url );
837 $report = array();
838 foreach( $this->xmlnodes as $k => $v ) {
839 switch( $v['tag'] ) {
840 case 'DAV::response':
841 if ( $v['type'] == 'open' ) {
842 $response = array();
844 elseif ( $v['type'] == 'close' ) {
845 $report[] = $response;
847 break;
848 case 'DAV::href':
849 $response['href'] = basename( rawurldecode($v['value']) );
850 break;
851 case 'DAV::getetag':
852 $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
853 break;
854 case 'urn:ietf:params:xml:ns:caldav:calendar-data':
855 $response['data'] = $v['value'];
856 break;
859 return $report;
864 * Get the events in a range from $start to $finish. The dates should be in the
865 * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
866 * array of event arrays. Each event array will have a 'href', 'etag' and 'event'
867 * part, where the 'href' is relative to the calendar and the event contains the
868 * definition of the event in iCalendar format.
870 * @param timestamp $start The start time for the period
871 * @param timestamp $finish The finish time for the period
872 * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
874 * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
876 function GetEvents( $start = null, $finish = null, $relative_url = '' ) {
877 $filter = "";
878 if ( isset($start) && isset($finish) )
879 $range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
880 else
881 $range = '';
883 $filter = <<<EOFILTER
884 <C:filter>
885 <C:comp-filter name="VCALENDAR">
886 <C:comp-filter name="VEVENT">
887 $range
888 </C:comp-filter>
889 </C:comp-filter>
890 </C:filter>
891 EOFILTER;
893 return $this->DoCalendarQuery($filter, $relative_url);
898 * Get the todo's in a range from $start to $finish. The dates should be in the
899 * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
900 * array of event arrays. Each event array will have a 'href', 'etag' and 'event'
901 * part, where the 'href' is relative to the calendar and the event contains the
902 * definition of the event in iCalendar format.
904 * @param timestamp $start The start time for the period
905 * @param timestamp $finish The finish time for the period
906 * @param boolean $completed Whether to include completed tasks
907 * @param boolean $cancelled Whether to include cancelled tasks
908 * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
910 * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
912 function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = "" ) {
914 if ( $start && $finish ) {
915 $time_range = <<<EOTIME
916 <C:time-range start="$start" end="$finish"/>
917 EOTIME;
920 // Warning! May contain traces of double negatives...
921 $neg_cancelled = ( $cancelled === true ? "no" : "yes" );
922 $neg_completed = ( $cancelled === true ? "no" : "yes" );
924 $filter = <<<EOFILTER
925 <C:filter>
926 <C:comp-filter name="VCALENDAR">
927 <C:comp-filter name="VTODO">
928 <C:prop-filter name="STATUS">
929 <C:text-match negate-condition="$neg_completed">COMPLETED</C:text-match>
930 </C:prop-filter>
931 <C:prop-filter name="STATUS">
932 <C:text-match negate-condition="$neg_cancelled">CANCELLED</C:text-match>
933 </C:prop-filter>$time_range
934 </C:comp-filter>
935 </C:comp-filter>
936 </C:filter>
937 EOFILTER;
939 return $this->DoCalendarQuery($filter, $relative_url);
944 * Get the calendar entry by UID
946 * @param uid
947 * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
948 * @param string $component_type The component type inside the VCALENDAR. Default 'VEVENT'.
950 * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery()
952 function GetEntryByUid( $uid, $relative_url = '', $component_type = 'VEVENT' ) {
953 $filter = "";
954 if ( $uid ) {
955 $filter = <<<EOFILTER
956 <C:filter>
957 <C:comp-filter name="VCALENDAR">
958 <C:comp-filter name="$component_type">
959 <C:prop-filter name="UID">
960 <C:text-match icollation="i;octet">$uid</C:text-match>
961 </C:prop-filter>
962 </C:comp-filter>
963 </C:comp-filter>
964 </C:filter>
965 EOFILTER;
968 return $this->DoCalendarQuery($filter, $relative_url);
973 * Get the calendar entry by HREF
975 * @param string $href The href from a call to GetEvents or GetTodos etc.
977 * @return string The iCalendar of the calendar entry
979 function GetEntryByHref( $href ) {
980 $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
981 return $this->DoGETRequest( $href );
987 * Usage example
989 * $cal = new CalDAVClient( "http://calendar.example.com/caldav.php/username/calendar/", "username", "password", "calendar" );
990 * $options = $cal->DoOptionsRequest();
991 * if ( isset($options["PROPFIND"]) ) {
992 * // Fetch some information about the events in that calendar
993 * $cal->SetDepth(1);
994 * $folder_xml = $cal->DoXMLRequest("PROPFIND", '<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><getcontentlength/><getcontenttype/><resourcetype/><getetag/></prop></propfind>' );
996 * // Fetch all events for February
997 * $events = $cal->GetEvents("20070101T000000Z","20070201T000000Z");
998 * foreach ( $events AS $k => $event ) {
999 * do_something_with_event_data( $event['data'] );
1001 * $acc = array();
1002 * $acc["google"] = array(
1003 * "user"=>"kunsttherapie@gmail.com",
1004 * "pass"=>"xxxxx",
1005 * "server"=>"ssl://www.google.com",
1006 * "port"=>"443",
1007 * "uri"=>"https://www.google.com/calendar/dav/kunsttherapie@gmail.com/events/",
1008 * );
1010 * $acc["davical"] = array(
1011 * "user"=>"some_user",
1012 * "pass"=>"big secret",
1013 * "server"=>"calendar.foo.bar",
1014 * "port"=>"80",
1015 * "uri"=>"http://calendar.foo.bar/caldav.php/some_user/home/",
1016 * );
1017 * //*******************************
1019 * $account = $acc["davical"];
1021 * //*******************************
1022 * $cal = new CalDAVClient( $account["uri"], $account["user"], $account["pass"], "", $account["server"], $account["port"] );
1023 * $options = $cal->DoOptionsRequest();
1024 * print_r($options);
1026 * //*******************************
1027 * //*******************************
1029 * $xmlC = <<<PROPP
1030 * <?xml version="1.0" encoding="utf-8" ?>
1031 * <D:propfind xmlns:D="DAV:" xmlns:C="http://calendarserver.org/ns/">
1032 * <D:prop>
1033 * <D:displayname />
1034 * <C:getctag />
1035 * <D:resourcetype />
1037 * </D:prop>
1038 * </D:propfind>
1039 * PROPP;
1040 * //if ( isset($options["PROPFIND"]) ) {
1041 * // Fetch some information about the events in that calendar
1042 * // $cal->SetDepth(1);
1043 * // $folder_xml = $cal->DoXMLRequest("PROPFIND", $xmlC);
1044 * // print_r( $folder_xml);
1045 * //}
1047 * // Fetch all events for February
1048 * $events = $cal->GetEvents("20090201T000000Z","20090301T000000Z");
1049 * foreach ( $events as $k => $event ) {
1050 * print_r($event['data']);
1051 * print "\n---------------------------------------------\n";
1054 * //*******************************
1055 * //*******************************