Merge branch 'master' of github.com:DAViCal/davical into github
[davical.git] / inc / caldav-client-v2.php
blob8dcf258a3a70ca6f719c0065b8b67e876135e42f
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 */
13 require_once('XMLDocument.php');
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;
24   function __construct( $url, $displayname = null, $getctag = null ) {
25     $this->url = $url;
26     $this->displayname = $displayname;
27     $this->getctag = $getctag;
28   }
30   function __toString() {
31     return( '(URL: '.$this->url.'   Ctag: '.$this->getctag.'   Displayname: '.$this->displayname .')'. "\n" );
32   }
33 }
35 if(!defined("_FSOCK_TIMEOUT")){
36   define("_FSOCK_TIMEOUT", 10);
37 }
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;
52   /**
53   * The principal-URL we're using
54   */
55   protected $principal_url;
57   /**
58   * The calendar-URL we're using
59   */
60   protected $calendar_url;
62   /**
63   * The calendar-home-set we're using
64   */
65   protected $calendar_home_set;
67   /**
68   * The calendar_urls we have discovered
69   */
70   protected $calendar_urls;
72   /**
73   * The useragent which is send to the caldav server
74   *
75   * @var string
76   */
77   public $user_agent = 'DAViCalClient';
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 = "";  
90   protected $parser; // our XML parser object
91   
92   private $debug = false; // Whether we are debugging
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();
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   }
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   }
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   }
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   }
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   }
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   }
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   }
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 );
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       }
212       xml_parser_free($parser);
213     }
214   }
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   }
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   }
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();
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;
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;
318     $this->xmlResponse = '';
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);
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;
334     $this->headers = array();  // reset the headers array for our next request
335     $this->ParseResponse($this->httpResponseBody);
336     return $response;
337   }
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   }
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   }
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   }
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   }
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   }
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;
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);
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   }
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 = "";
483     $this->requestMethod = "DELETE";
484     if ( $etag != null ) {
485       $this->SetMatch( true, $etag );
486     }
487     $this->DoRequest($url);
488     return $this->httpResponseCode;
489   }
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     }
505     $this->body = $xml->Render('propfind',$prop );
507     $this->requestMethod = "PROPFIND";
508     $this->SetContentType("text/xml");
509     $this->DoRequest($url);
510     return $this->GetXmlResponse();
511   }
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   }
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   }
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   }
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   }
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   }
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   }
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   }
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);
660     $principal_url = $this->HrefForProp('DAV::principal');
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     }
670     return $this->PrincipalURL($principal_url);
671   }
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     }
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     }
697     if ( !$recursed && count($calendar_home) < 1 ) {
698       $calendar_home = $this->FindCalendarHome(true);
699     }
701     return $this->CalendarHomeSet($calendar_home);
702   }
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);
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       }
721       foreach( $this->xmltags['DAV::href'] AS $i => $hnode ) {
722         $href = rawurldecode($this->xmlnodes[$hnode]['value']);
724         if ( !isset($calendar_urls[$href]) ) continue;
726 //        printf("Seems '%s' is a calendar.\n", $href );
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     }
745     return $this->CalendarUrls($calendars);
746   }
749   /**
750   * Find the calendars, from the calendar_home_set
751   */
752   function GetCalendarDetails( $url = null ) {
753     if ( isset($url) ) $this->SetCalendar($url);
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);
758     $hnode = $this->xmltags['DAV::href'][0];
759     $href = rawurldecode($this->xmlnodes[$hnode]['value']);
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     }
773     return $calendar;
774   }
777   /**
778   * Get all etags for a calendar
779   */
780   function GetCollectionETags( $url = null ) {
781     if ( isset($url) ) $this->SetCalendar($url);
783     $this->DoPROPFINDRequest( $this->calendar_url, array('getetag'), 1);
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     }
793     return $etags;
794   }
797   /**
798   * Get a bunch of events for a calendar with a calendar-multiget report
799   */
800   function CalendarMultiget( $event_hrefs, $url = null ) {
802     if ( isset($url) ) $this->SetCalendar($url);
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;
817     $this->requestMethod = "REPORT";
818     $this->SetContentType("text/xml");
819     $this->DoRequest( $this->calendar_url );
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     }
836     return $events;
837   }
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 = '' ) {
855     if ( !empty($url) ) $this->SetCalendar($url);
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;
867     $this->requestMethod = "REPORT";
868     $this->SetContentType("text/xml");
869     $this->DoRequest( $this->calendar_url );
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   }
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 = '';
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;
927     return $this->DoCalendarQuery($filter, $relative_url);
928   }
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 = "" ) {
948     if ( $start && $finish ) {
949 $time_range = <<<EOTIME
950                 <C:time-range start="$start" end="$finish"/>
951 EOTIME;
952     }
954     // Warning!  May contain traces of double negatives...
955     $neg_cancelled = ( $cancelled === true ? "no" : "yes" );
956     $neg_completed = ( $cancelled === true ? "no" : "yes" );
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;
973     return $this->DoCalendarQuery($filter, $relative_url);
974   }
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     }
1002     return $this->DoCalendarQuery($filter, $relative_url);
1003   }
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   }