Merge branch 'master' of github.com:DAViCal/davical into github
[davical.git] / inc / caldav-client-v2.php
1 <?php
2 /**
3 * A Class for connecting to a caldav server
4 *
5 * @package awl
6 *
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
11 */
12
13 require_once('XMLDocument.php');
14
15 /**
16 * A class for holding basic calendar information
17 * @package awl
18 */
19 class CalendarInfo {
20 public $url;
21 public $displayname;
22 public $getctag;
23
24 function __construct( $url, $displayname = null, $getctag = null ) {
25 $this->url = $url;
26 $this->displayname = $displayname;
27 $this->getctag = $getctag;
28 }
29
30 function __toString() {
31 return( '(URL: '.$this->url.' Ctag: '.$this->getctag.' Displayname: '.$this->displayname .')'. "\n" );
32 }
33 }
34
35 if(!defined("_FSOCK_TIMEOUT")){
36 define("_FSOCK_TIMEOUT", 10);
37 }
38
39 /**
40 * A class for accessing DAViCal via CalDAV, as a client
41 *
42 * @package awl
43 */
44 class CalDAVClient {
45 /**
46 * Server, username, password, calendar
47 *
48 * @var string
49 */
50 protected $base_url, $user, $pass, $entry, $protocol, $server, $port;
51
52 /**
53 * The principal-URL we're using
54 */
55 protected $principal_url;
56
57 /**
58 * The calendar-URL we're using
59 */
60 protected $calendar_url;
61
62 /**
63 * The calendar-home-set we're using
64 */
65 protected $calendar_home_set;
66
67 /**
68 * The calendar_urls we have discovered
69 */
70 protected $calendar_urls;
71
72 /**
73 * The useragent which is send to the caldav server
74 *
75 * @var string
76 */
77 public $user_agent = 'DAViCalClient';
78
79 protected $headers = array();
80 protected $body = "";
81 protected $requestMethod = "GET";
82 protected $httpRequest = ""; // for debugging http headers sent
83 protected $xmlRequest = ""; // for debugging xml sent
84 protected $xmlResponse = ""; // xml received
85 protected $httpResponseCode = 0; // http response code
86 protected $httpResponseHeaders = "";
87 protected $httpParsedHeaders;
88 protected $httpResponseBody = "";
89
90 protected $parser; // our XML parser object
91
92 private $debug = false; // Whether we are debugging
93
94 /**
95 * Constructor, initialises the class
96 *
97 * @param string $base_url The URL for the calendar server
98 * @param string $user The name of the user logging in
99 * @param string $pass The password for that user
100 */
101 function __construct( $base_url, $user, $pass ) {
102 $this->user = $user;
103 $this->pass = $pass;
104 $this->headers = array();
105
106 if ( preg_match( '#^(https?)://([a-z0-9.-]+)(:([0-9]+))?(/.*)$#', $base_url, $matches ) ) {
107 $this->server = $matches[2];
108 $this->base_url = $matches[5];
109 if ( $matches[1] == 'https' ) {
110 $this->protocol = 'ssl';
111 $this->port = 443;
112 }
113 else {
114 $this->protocol = 'tcp';
115 $this->port = 80;
116 }
117 if ( $matches[4] != '' ) {
118 $this->port = intval($matches[4]);
119 }
120 }
121 else {
122 trigger_error("Invalid URL: '".$base_url."'", E_USER_ERROR);
123 }
124 }
125
126
127 /**
128 * Call this to enable / disable debugging. It will return the prior value of the debugging flag.
129 * @param boolean $new_value The new value for debugging.
130 * @return boolean The previous value, in case you want to restore it later.
131 */
132 function SetDebug( $new_value ) {
133 $old_value = $this->debug;
134 if ( $new_value )
135 $this->debug = true;
136 else
137 $this->debug = false;
138 return $old_value;
139 }
140
141
142
143 /**
144 * Adds an If-Match or If-None-Match header
145 *
146 * @param bool $match to Match or Not to Match, that is the question!
147 * @param string $etag The etag to match / not match against.
148 */
149 function SetMatch( $match, $etag = '*' ) {
150 $this->headers['match'] = sprintf( "%s-Match: \"%s\"", ($match ? "If" : "If-None"), trim($etag,'"'));
151 }
152
153 /**
154 * Add a Depth: header. Valid values are 0, 1 or infinity
155 *
156 * @param int $depth The depth, default to infinity
157 */
158 function SetDepth( $depth = '0' ) {
159 $this->headers['depth'] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") );
160 }
161
162 /**
163 * Add a Depth: header. Valid values are 1 or infinity
164 *
165 * @param int $depth The depth, default to infinity
166 */
167 function SetUserAgent( $user_agent = null ) {
168 if ( !isset($user_agent) ) $user_agent = $this->user_agent;
169 $this->user_agent = $user_agent;
170 }
171
172 /**
173 * Add a Content-type: header.
174 *
175 * @param string $type The content type
176 */
177 function SetContentType( $type ) {
178 $this->headers['content-type'] = "Content-type: $type";
179 }
180
181 /**
182 * Set the calendar_url we will be using for a while.
183 *
184 * @param string $url The calendar_url
185 */
186 function SetCalendar( $url ) {
187 $this->calendar_url = $url;
188 }
189
190 /**
191 * Split response into httpResponse and xmlResponse
192 *
193 * @param string Response from server
194 */
195 function ParseResponse( $response ) {
196 $pos = strpos($response, '<?xml');
197 if ($pos !== false) {
198 $this->xmlResponse = trim(substr($response, $pos));
199 $this->xmlResponse = preg_replace('{>[^>]*$}s', '>',$this->xmlResponse );
200 $parser = xml_parser_create_ns('UTF-8');
201 xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 );
202 xml_parser_set_option ( $parser, XML_OPTION_CASE_FOLDING, 0 );
203
204 if ( xml_parse_into_struct( $parser, $this->xmlResponse, $this->xmlnodes, $this->xmltags ) === 0 ) {
205 printf( "XML parsing error: %s - %s\n", xml_get_error_code($parser), xml_error_string(xml_get_error_code($parser)) );
206 // debug_print_backtrace();
207 // echo "\nNodes array............................................................\n"; print_r( $this->xmlnodes );
208 // echo "\nTags array............................................................\n"; print_r( $this->xmltags );
209 printf( "\nXML Reponse:\n%s\n", $this->xmlResponse );
210 }
211
212 xml_parser_free($parser);
213 }
214 }
215
216 /**
217 * Split httpResponseHeaders into an array of headers
218 *
219 * @return array of arrays of header lines
220 */
221 function ParseResponseHeaders() {
222 if ( empty($this->httpResponseHeaders) ) return array();
223 if ( !isset($this->httpParsedHeaders) ) {
224 $this->httpParsedHeaders = array();
225 $headers = str_replace("\r\n", "\n", $this->httpResponseHeaders);
226 $ar_headers = explode("\n", $headers);
227 $last_header = '';
228 foreach ($ar_headers as $cur_headers) {
229 if( preg_match( '{^\s*\S}', $cur_headers) ) $header_name = $last_header;
230 else if ( preg_match( '{^(\S*):', $cur_headers, $matches) ) {
231 $header_name = $matches[1];
232 $last_header = $header_name;
233 if ( empty($this->httpParsedHeaders[$header_name]) ) $this->httpParsedHeaders[$header_name] = array();
234 }
235 $this->httpParsedHeaders[$header_name][] = $cur_headers;
236 }
237 }
238 return $this->httpParsedHeaders;
239 }
240
241 /**
242 * Output http request headers
243 *
244 * @return HTTP headers
245 */
246 function GetHttpRequest() {
247 return $this->httpRequest;
248 }
249 /**
250 * Output http response headers
251 *
252 * @return HTTP headers
253 */
254 function GetResponseHeaders() {
255 return $this->httpResponseHeaders;
256 }
257 /**
258 * Output http response body
259 *
260 * @return HTTP body
261 */
262 function GetResponseBody() {
263 return $this->httpResponseBody;
264 }
265 /**
266 * Output xml request
267 *
268 * @return raw xml
269 */
270 function GetXmlRequest() {
271 return $this->xmlRequest;
272 }
273 /**
274 * Output xml response
275 *
276 * @return raw xml
277 */
278 function GetXmlResponse() {
279 return $this->xmlResponse;
280 }
281
282 /**
283 * Send a request to the server
284 *
285 * @param string $url The URL to make the request to
286 *
287 * @return string The content of the response from the server
288 */
289 function DoRequest( $url = null ) {
290 $headers = array();
291
292 if ( !isset($url) ) $url = $this->base_url;
293 $this->request_url = $url;
294 $url = preg_replace('{^https?://[^/]+}', '', $url);
295 // URLencode if it isn't already
296 if ( preg_match( '{[^%?&=+,.-_/a-z0-9]}', $url ) ) {
297 $url = str_replace(rawurlencode('/'),'/',rawurlencode($url));
298 $url = str_replace(rawurlencode('?'),'?',$url);
299 $url = str_replace(rawurlencode('&'),'&',$url);
300 $url = str_replace(rawurlencode('='),'=',$url);
301 $url = str_replace(rawurlencode('+'),'+',$url);
302 $url = str_replace(rawurlencode(','),',',$url);
303 }
304 $headers[] = $this->requestMethod." ". $url . " HTTP/1.1";
305 $headers[] = "Authorization: Basic ".base64_encode($this->user .":". $this->pass );
306 $headers[] = "Host: ".$this->server .":".$this->port;
307
308 if ( !isset($this->headers['content-type']) ) $this->headers['content-type'] = "Content-type: text/plain";
309 foreach( $this->headers as $ii => $head ) {
310 $headers[] = $head;
311 }
312 $headers[] = "Content-Length: " . strlen($this->body);
313 $headers[] = "User-Agent: " . $this->user_agent;
314 $headers[] = 'Connection: close';
315 $this->httpRequest = join("\r\n",$headers);
316 $this->xmlRequest = $this->body;
317
318 $this->xmlResponse = '';
319
320 $fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT); //error handling?
321 if ( !(get_resource_type($fip) == 'stream') ) return false;
322 if ( !fwrite($fip, $this->httpRequest."\r\n\r\n".$this->body) ) { fclose($fip); return false; }
323 $response = "";
324 while( !feof($fip) ) { $response .= fgets($fip,8192); }
325 fclose($fip);
326
327 list( $this->httpResponseHeaders, $this->httpResponseBody ) = preg_split( '{\r?\n\r?\n}s', $response, 2 );
328 if ( preg_match( '{Transfer-Encoding: chunked}i', $this->httpResponseHeaders ) ) $this->Unchunk();
329 if ( preg_match('/HTTP\/\d\.\d (\d{3})/', $this->httpResponseHeaders, $status) )
330 $this->httpResponseCode = intval($status[1]);
331 else
332 $this->httpResponseCode = 0;
333
334 $this->headers = array(); // reset the headers array for our next request
335 $this->ParseResponse($this->httpResponseBody);
336 return $response;
337 }
338
339
340 /**
341 * Unchunk a chunked response
342 */
343 function Unchunk() {
344 $content = '';
345 $chunks = $this->httpResponseBody;
346 // printf( "\n================================\n%s\n================================\n", $chunks );
347 do {
348 $bytes = 0;
349 if ( preg_match('{^((\r\n)?\s*([ 0-9a-fA-F]+)(;[^\n]*)?\r?\n)}', $chunks, $matches ) ) {
350 $octets = $matches[3];
351 $bytes = hexdec($octets);
352 $pos = strlen($matches[1]);
353 // printf( "Chunk size 0x%s (%d)\n", $octets, $bytes );
354 if ( $bytes > 0 ) {
355 // printf( "---------------------------------\n%s\n---------------------------------\n", substr($chunks,$pos,$bytes) );
356 $content .= substr($chunks,$pos,$bytes);
357 $chunks = substr($chunks,$pos + $bytes + 2);
358 // printf( "+++++++++++++++++++++++++++++++++\n%s\n+++++++++++++++++++++++++++++++++\n", $chunks );
359 }
360 }
361 else {
362 $content .= $chunks;
363 }
364 }
365 while( $bytes > 0 );
366 $this->httpResponseBody = $content;
367 // printf( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n%s\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", $content );
368 }
369
370
371 /**
372 * Send an OPTIONS request to the server
373 *
374 * @param string $url The URL to make the request to
375 *
376 * @return array The allowed options
377 */
378 function DoOptionsRequest( $url = null ) {
379 $this->requestMethod = "OPTIONS";
380 $this->body = "";
381 $this->DoRequest($url);
382 $this->ParseResponseHeaders();
383 $allowed = '';
384 foreach( $this->httpParsedHeaders['Allow'] as $allow_header ) {
385 $allowed .= preg_replace( '/^(Allow:)?\s+([a-z, ]+)\r?\n.*/is', '$1,', $allow_header );
386 }
387 $options = array_flip( preg_split( '/[, ]+/', trim($allowed, ', ') ));
388 return $options;
389 }
390
391
392
393 /**
394 * Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR)
395 *
396 * @param string $method The method (PROPFIND, REPORT, etc) to use with the request
397 * @param string $xml The XML to send along with the request
398 * @param string $url The URL to make the request to
399 *
400 * @return array An array of the allowed methods
401 */
402 function DoXMLRequest( $request_method, $xml, $url = null ) {
403 $this->body = $xml;
404 $this->requestMethod = $request_method;
405 $this->SetContentType("text/xml");
406 return $this->DoRequest($url);
407 }
408
409
410
411 /**
412 * Get a single item from the server.
413 *
414 * @param string $url The URL to GET
415 */
416 function DoGETRequest( $url ) {
417 $this->body = "";
418 $this->requestMethod = "GET";
419 return $this->DoRequest( $url );
420 }
421
422
423 /**
424 * Get the HEAD of a single item from the server.
425 *
426 * @param string $url The URL to HEAD
427 */
428 function DoHEADRequest( $url ) {
429 $this->body = "";
430 $this->requestMethod = "HEAD";
431 return $this->DoRequest( $url );
432 }
433
434
435 /**
436 * PUT a text/icalendar resource, returning the etag
437 *
438 * @param string $url The URL to make the request to
439 * @param string $icalendar The iCalendar resource to send to the server
440 * @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource.
441 *
442 * @return string The content of the response from the server
443 */
444 function DoPUTRequest( $url, $icalendar, $etag = null ) {
445 $this->body = $icalendar;
446
447 $this->requestMethod = "PUT";
448 if ( $etag != null ) {
449 $this->SetMatch( ($etag != '*'), $etag );
450 }
451 $this->SetContentType('text/calendar; charset="utf-8"');
452 $this->DoRequest($url);
453
454 $etag = null;
455 if ( preg_match( '{^ETag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1];
456 if ( !isset($etag) || $etag == '' ) {
457 if ( $this->debug ) printf( "No etag in:\n%s\n", $this->httpResponseHeaders );
458 $save_request = $this->httpRequest;
459 $save_response_headers = $this->httpResponseHeaders;
460 $this->DoHEADRequest( $url );
461 if ( preg_match( '{^Etag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1];
462 if ( !isset($etag) || $etag == '' ) {
463 if ( $this->debug ) printf( "Still No etag in:\n%s\n", $this->httpResponseHeaders );
464 }
465 $this->httpRequest = $save_request;
466 $this->httpResponseHeaders = $save_response_headers;
467 }
468 return $etag;
469 }
470
471
472 /**
473 * DELETE a text/icalendar resource
474 *
475 * @param string $url The URL to make the request to
476 * @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL.
477 *
478 * @return int The HTTP Result Code for the DELETE
479 */
480 function DoDELETERequest( $url, $etag = null ) {
481 $this->body = "";
482
483 $this->requestMethod = "DELETE";
484 if ( $etag != null ) {
485 $this->SetMatch( true, $etag );
486 }
487 $this->DoRequest($url);
488 return $this->httpResponseCode;
489 }
490
491
492 /**
493 * Get a single item from the server.
494 *
495 * @param string $url The URL to PROPFIND on
496 */
497 function DoPROPFINDRequest( $url, $props, $depth = 0 ) {
498 $this->SetDepth($depth);
499 $xml = new XMLDocument( array( 'DAV:' => '', 'urn:ietf:params:xml:ns:caldav' => 'C' ) );
500 $prop = new XMLElement('prop');
501 foreach( $props AS $v ) {
502 $xml->NSElement($prop,$v);
503 }
504
505 $this->body = $xml->Render('propfind',$prop );
506
507 $this->requestMethod = "PROPFIND";
508 $this->SetContentType("text/xml");
509 $this->DoRequest($url);
510 return $this->GetXmlResponse();
511 }
512
513
514 /**
515 * Get/Set the Principal URL
516 *
517 * @param $url string The Principal URL to set
518 */
519 function PrincipalURL( $url = null ) {
520 if ( isset($url) ) {
521 $this->principal_url = $url;
522 }
523 return $this->principal_url;
524 }
525
526
527 /**
528 * Get/Set the calendar-home-set URL
529 *
530 * @param $url array of string The calendar-home-set URLs to set
531 */
532 function CalendarHomeSet( $urls = null ) {
533 if ( isset($urls) ) {
534 if ( ! is_array($urls) ) $urls = array($urls);
535 $this->calendar_home_set = $urls;
536 }
537 return $this->calendar_home_set;
538 }
539
540
541 /**
542 * Get/Set the calendar-home-set URL
543 *
544 * @param $urls array of string The calendar URLs to set
545 */
546 function CalendarUrls( $urls = null ) {
547 if ( isset($urls) ) {
548 if ( ! is_array($urls) ) $urls = array($urls);
549 $this->calendar_urls = $urls;
550 }
551 return $this->calendar_urls;
552 }
553
554
555 /**
556 * Return the first occurrence of an href inside the named tag.
557 *
558 * @param string $tagname The tag name to find the href inside of
559 */
560 function HrefValueInside( $tagname ) {
561 foreach( $this->xmltags[$tagname] AS $k => $v ) {
562 $j = $v + 1;
563 if ( $this->xmlnodes[$j]['tag'] == 'DAV::href' ) {
564 return rawurldecode($this->xmlnodes[$j]['value']);
565 }
566 }
567 return null;
568 }
569
570
571 /**
572 * Return the href containing this property. Except only if it's inside a status != 200
573 *
574 * @param string $tagname The tag name of the property to find the href for
575 * @param integer $which Which instance of the tag should we use
576 */
577 function HrefForProp( $tagname, $i = 0 ) {
578 if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
579 $j = $this->xmltags[$tagname][$i];
580 while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' ) {
581 // printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
582 if ( $this->xmlnodes[$j]['tag'] == 'DAV::status' && $this->xmlnodes[$j]['value'] != 'HTTP/1.1 200 OK' ) return null;
583 }
584 // printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
585 if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
586 // printf( "Value[$j]: %s\n", $this->xmlnodes[$j]['value']);
587 return rawurldecode($this->xmlnodes[$j]['value']);
588 }
589 }
590 else {
591 if ( $this->debug ) printf( "xmltags[$tagname] or xmltags[$tagname][$i] is not set\n");
592 }
593 return null;
594 }
595
596
597 /**
598 * Return the href which has a resourcetype of the specified type
599 *
600 * @param string $tagname The tag name of the resourcetype to find the href for
601 * @param integer $which Which instance of the tag should we use
602 */
603 function HrefForResourcetype( $tagname, $i = 0 ) {
604 if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
605 $j = $this->xmltags[$tagname][$i];
606 while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::resourcetype' );
607 if ( $j > 0 ) {
608 while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' );
609 if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
610 return rawurldecode($this->xmlnodes[$j]['value']);
611 }
612 }
613 }
614 return null;
615 }
616
617
618 /**
619 * Return the <prop> ... </prop> of a propstat where the status is OK
620 *
621 * @param string $nodenum The node number in the xmlnodes which is the href
622 */
623 function GetOKProps( $nodenum ) {
624 $props = null;
625 $level = $this->xmlnodes[$nodenum]['level'];
626 $status = '';
627 while ( $this->xmlnodes[++$nodenum]['level'] >= $level ) {
628 if ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::propstat' ) {
629 if ( $this->xmlnodes[$nodenum]['type'] == 'open' ) {
630 $props = array();
631 $status = '';
632 }
633 else {
634 if ( $status == 'HTTP/1.1 200 OK' ) break;
635 }
636 }
637 elseif ( !isset($this->xmlnodes[$nodenum]) || !is_array($this->xmlnodes[$nodenum]) ) {
638 break;
639 }
640 elseif ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::status' ) {
641 $status = $this->xmlnodes[$nodenum]['value'];
642 }
643 else {
644 $props[] = $this->xmlnodes[$nodenum];
645 }
646 }
647 return $props;
648 }
649
650
651 /**
652 * Attack the given URL in an attempt to find a principal URL
653 *
654 * @param string $url The URL to find the principal-URL from
655 */
656 function FindPrincipal( $url=null ) {
657 $xml = $this->DoPROPFINDRequest( $url, array('resourcetype', 'current-user-principal', 'owner', 'principal-URL',
658 'urn:ietf:params:xml:ns:caldav:calendar-home-set'), 1);
659
660 $principal_url = $this->HrefForProp('DAV::principal');
661
662 if ( !isset($principal_url) ) {
663 foreach( array('DAV::current-user-principal', 'DAV::principal-URL', 'DAV::owner') AS $href ) {
664 if ( !isset($principal_url) ) {
665 $principal_url = $this->HrefValueInside($href);
666 }
667 }
668 }
669
670 return $this->PrincipalURL($principal_url);
671 }
672
673
674 /**
675 * Attack the given URL in an attempt to find a principal URL
676 *
677 * @param string $url The URL to find the calendar-home-set from
678 */
679 function FindCalendarHome( $recursed=false ) {
680 if ( !isset($this->principal_url) ) {
681 $this->FindPrincipal();
682 }
683 if ( $recursed ) {
684 $this->DoPROPFINDRequest( $this->principal_url, array('urn:ietf:params:xml:ns:caldav:calendar-home-set'), 0);
685 }
686
687 $calendar_home = array();
688 foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-home-set'] AS $k => $v ) {
689 if ( $this->xmlnodes[$v]['type'] != 'open' ) continue;
690 while( $this->xmlnodes[++$v]['type'] != 'close' && $this->xmlnodes[$v]['tag'] != 'urn:ietf:params:xml:ns:caldav:calendar-home-set' ) {
691 // printf( "Tag: '%s' = '%s'\n", $this->xmlnodes[$v]['tag'], $this->xmlnodes[$v]['value']);
692 if ( $this->xmlnodes[$v]['tag'] == 'DAV::href' && isset($this->xmlnodes[$v]['value']) )
693 $calendar_home[] = rawurldecode($this->xmlnodes[$v]['value']);
694 }
695 }
696
697 if ( !$recursed && count($calendar_home) < 1 ) {
698 $calendar_home = $this->FindCalendarHome(true);
699 }
700
701 return $this->CalendarHomeSet($calendar_home);
702 }
703
704
705 /**
706 * Find the calendars, from the calendar_home_set
707 */
708 function FindCalendars( $recursed=false ) {
709 if ( !isset($this->calendar_home_set[0]) ) {
710 $this->FindCalendarHome();
711 }
712 $this->DoPROPFINDRequest( $this->calendar_home_set[0], array('resourcetype','displayname','http://calendarserver.org/ns/:getctag'), 1);
713
714 $calendars = array();
715 if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar']) ) {
716 $calendar_urls = array();
717 foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar'] AS $k => $v ) {
718 $calendar_urls[$this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar', $k)] = 1;
719 }
720
721 foreach( $this->xmltags['DAV::href'] AS $i => $hnode ) {
722 $href = rawurldecode($this->xmlnodes[$hnode]['value']);
723
724 if ( !isset($calendar_urls[$href]) ) continue;
725
726 // printf("Seems '%s' is a calendar.\n", $href );
727
728 $calendar = new CalendarInfo($href);
729 $ok_props = $this->GetOKProps($hnode);
730 foreach( $ok_props AS $v ) {
731 // printf("Looking at: %s[%s]\n", $href, $v['tag'] );
732 switch( $v['tag'] ) {
733 case 'http://calendarserver.org/ns/:getctag':
734 $calendar->getctag = $v['value'];
735 break;
736 case 'DAV::displayname':
737 $calendar->displayname = $v['value'];
738 break;
739 }
740 }
741 $calendars[] = $calendar;
742 }
743 }
744
745 return $this->CalendarUrls($calendars);
746 }
747
748
749 /**
750 * Find the calendars, from the calendar_home_set
751 */
752 function GetCalendarDetails( $url = null ) {
753 if ( isset($url) ) $this->SetCalendar($url);
754
755 $calendar_properties = array( 'resourcetype', 'displayname', 'http://calendarserver.org/ns/:getctag', 'urn:ietf:params:xml:ns:caldav:calendar-timezone', 'supported-report-set' );
756 $this->DoPROPFINDRequest( $this->calendar_url, $calendar_properties, 0);
757
758 $hnode = $this->xmltags['DAV::href'][0];
759 $href = rawurldecode($this->xmlnodes[$hnode]['value']);
760
761 $calendar = new CalendarInfo($href);
762 $ok_props = $this->GetOKProps($hnode);
763 foreach( $ok_props AS $k => $v ) {
764 $name = preg_replace( '{^.*:}', '', $v['tag'] );
765 if ( isset($v['value'] ) ) {
766 $calendar->{$name} = $v['value'];
767 }
768 /* else {
769 printf( "Calendar property '%s' has no text content\n", $v['tag'] );
770 }*/
771 }
772
773 return $calendar;
774 }
775
776
777 /**
778 * Get all etags for a calendar
779 */
780 function GetCollectionETags( $url = null ) {
781 if ( isset($url) ) $this->SetCalendar($url);
782
783 $this->DoPROPFINDRequest( $this->calendar_url, array('getetag'), 1);
784
785 $etags = array();
786 if ( isset($this->xmltags['DAV::getetag']) ) {
787 foreach( $this->xmltags['DAV::getetag'] AS $k => $v ) {
788 $href = $this->HrefForProp('DAV::getetag', $k);
789 if ( isset($href) && isset($this->xmlnodes[$v]['value']) ) $etags[$href] = $this->xmlnodes[$v]['value'];
790 }
791 }
792
793 return $etags;
794 }
795
796
797 /**
798 * Get a bunch of events for a calendar with a calendar-multiget report
799 */
800 function CalendarMultiget( $event_hrefs, $url = null ) {
801
802 if ( isset($url) ) $this->SetCalendar($url);
803
804 $hrefs = '';
805 foreach( $event_hrefs AS $k => $href ) {
806 $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
807 $hrefs .= '<href>'.$href.'</href>';
808 }
809 $this->body = <<<EOXML
810 <?xml version="1.0" encoding="utf-8" ?>
811 <C:calendar-multiget xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
812 <prop><getetag/><C:calendar-data/></prop>
813 $hrefs
814 </C:calendar-multiget>
815 EOXML;
816
817 $this->requestMethod = "REPORT";
818 $this->SetContentType("text/xml");
819 $this->DoRequest( $this->calendar_url );
820
821 $events = array();
822 if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data']) ) {
823 foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data'] AS $k => $v ) {
824 $href = $this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar-data', $k);
825 // echo "Calendar-data:\n"; print_r($this->xmlnodes[$v]);
826 $events[$href] = $this->xmlnodes[$v]['value'];
827 }
828 }
829 else {
830 foreach( $event_hrefs AS $k => $href ) {
831 $this->DoGETRequest($href);
832 $events[$href] = $this->httpResponseBody;
833 }
834 }
835
836 return $events;
837 }
838
839
840 /**
841 * Given XML for a calendar query, return an array of the events (/todos) in the
842 * response. Each event in the array will have a 'href', 'etag' and '$response_type'
843 * part, where the 'href' is relative to the calendar and the '$response_type' contains the
844 * definition of the calendar data in iCalendar format.
845 *
846 * @param string $filter XML fragment which is the <filter> element of a calendar-query
847 * @param string $url The URL of the calendar, or empty/null to use the 'current' calendar_url
848 *
849 * @return array An array of the relative URLs, etags, and events from the server. Each element of the array will
850 * be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied
851 * etag (which only varies when the data changes) and the calendar data in iCalendar format.
852 */
853 function DoCalendarQuery( $filter, $url = '' ) {
854
855 if ( !empty($url) ) $this->SetCalendar($url);
856
857 $this->body = <<<EOXML
858 <?xml version="1.0" encoding="utf-8" ?>
859 <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
860 <D:prop>
861 <C:calendar-data/>
862 <D:getetag/>
863 </D:prop>$filter
864 </C:calendar-query>
865 EOXML;
866
867 $this->requestMethod = "REPORT";
868 $this->SetContentType("text/xml");
869 $this->DoRequest( $this->calendar_url );
870
871 $report = array();
872 foreach( $this->xmlnodes as $k => $v ) {
873 switch( $v['tag'] ) {
874 case 'DAV::response':
875 if ( $v['type'] == 'open' ) {
876 $response = array();
877 }
878 elseif ( $v['type'] == 'close' ) {
879 $report[] = $response;
880 }
881 break;
882 case 'DAV::href':
883 $response['href'] = basename( rawurldecode($v['value']) );
884 break;
885 case 'DAV::getetag':
886 $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
887 break;
888 case 'urn:ietf:params:xml:ns:caldav:calendar-data':
889 $response['data'] = $v['value'];
890 break;
891 }
892 }
893 return $report;
894 }
895
896
897 /**
898 * Get the events 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.
903 *
904 * @param timestamp $start The start time for the period
905 * @param timestamp $finish The finish time for the period
906 * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
907 *
908 * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
909 */
910 function GetEvents( $start = null, $finish = null, $relative_url = '' ) {
911 $filter = "";
912 if ( isset($start) && isset($finish) )
913 $range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
914 else
915 $range = '';
916
917 $filter = <<<EOFILTER
918 <C:filter>
919 <C:comp-filter name="VCALENDAR">
920 <C:comp-filter name="VEVENT">
921 $range
922 </C:comp-filter>
923 </C:comp-filter>
924 </C:filter>
925 EOFILTER;
926
927 return $this->DoCalendarQuery($filter, $relative_url);
928 }
929
930
931 /**
932 * Get the todo's in a range from $start to $finish. The dates should be in the
933 * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
934 * array of event arrays. Each event array will have a 'href', 'etag' and 'event'
935 * part, where the 'href' is relative to the calendar and the event contains the
936 * definition of the event in iCalendar format.
937 *
938 * @param timestamp $start The start time for the period
939 * @param timestamp $finish The finish time for the period
940 * @param boolean $completed Whether to include completed tasks
941 * @param boolean $cancelled Whether to include cancelled tasks
942 * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
943 *
944 * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
945 */
946 function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = "" ) {
947
948 if ( $start && $finish ) {
949 $time_range = <<<EOTIME
950 <C:time-range start="$start" end="$finish"/>
951 EOTIME;
952 }
953
954 // Warning! May contain traces of double negatives...
955 $neg_cancelled = ( $cancelled === true ? "no" : "yes" );
956 $neg_completed = ( $cancelled === true ? "no" : "yes" );
957
958 $filter = <<<EOFILTER
959 <C:filter>
960 <C:comp-filter name="VCALENDAR">
961 <C:comp-filter name="VTODO">
962 <C:prop-filter name="STATUS">
963 <C:text-match negate-condition="$neg_completed">COMPLETED</C:text-match>
964 </C:prop-filter>
965 <C:prop-filter name="STATUS">
966 <C:text-match negate-condition="$neg_cancelled">CANCELLED</C:text-match>
967 </C:prop-filter>$time_range
968 </C:comp-filter>
969 </C:comp-filter>
970 </C:filter>
971 EOFILTER;
972
973 return $this->DoCalendarQuery($filter, $relative_url);
974 }
975
976
977 /**
978 * Get the calendar entry by UID
979 *
980 * @param uid
981 * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
982 * @param string $component_type The component type inside the VCALENDAR. Default 'VEVENT'.
983 *
984 * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery()
985 */
986 function GetEntryByUid( $uid, $relative_url = '', $component_type = 'VEVENT' ) {
987 $filter = "";
988 if ( $uid ) {
989 $filter = <<<EOFILTER
990 <C:filter>
991 <C:comp-filter name="VCALENDAR">
992 <C:comp-filter name="$component_type">
993 <C:prop-filter name="UID">
994 <C:text-match icollation="i;octet">$uid</C:text-match>
995 </C:prop-filter>
996 </C:comp-filter>
997 </C:comp-filter>
998 </C:filter>
999 EOFILTER;
1000 }
1001
1002 return $this->DoCalendarQuery($filter, $relative_url);
1003 }
1004
1005
1006 /**
1007 * Get the calendar entry by HREF
1008 *
1009 * @param string $href The href from a call to GetEvents or GetTodos etc.
1010 *
1011 * @return string The iCalendar of the calendar entry
1012 */
1013 function GetEntryByHref( $href ) {
1014 $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
1015 return $this->DoGETRequest( $href );
1016 }
1017
1018 }