DAViCal
caldav-client.php
1<?php
20class CalDAVClient {
26 var $base_url, $user, $pass, $calendar, $entry, $protocol, $server, $port;
27
33 var $user_agent = 'DAViCalClient';
34
35 var $headers = array();
36 var $body = "";
37 var $requestMethod = "GET";
38 var $httpRequest = ""; // for debugging http headers sent
39 var $xmlRequest = ""; // for debugging xml sent
40 var $httpResponse = ""; // for debugging http headers received
41 var $xmlResponse = ""; // for debugging xml received
42
51 function __construct( $base_url, $user, $pass, $calendar = '' ) {
52 $this->user = $user;
53 $this->pass = $pass;
54 $this->calendar = $calendar;
55 $this->headers = array();
56
57 if ( preg_match( '#^(https?)://([a-z0-9.-]+)(:([0-9]+))?(/.*)$#', $base_url, $matches ) ) {
58 $this->server = $matches[2];
59 $this->base_url = $matches[5];
60 if ( $matches[1] == 'https' ) {
61 $this->protocol = 'ssl';
62 $this->port = 443;
63 }
64 else {
65 $this->protocol = 'tcp';
66 $this->port = 80;
67 }
68 if ( $matches[4] != '' ) {
69 $this->port = intval($matches[4]);
70 }
71 }
72 else {
73 trigger_error("Invalid URL: '".$base_url."'", E_USER_ERROR);
74 }
75 }
76
83 function SetMatch( $match, $etag = '*' ) {
84 $this->headers[] = sprintf( "%s-Match: %s", ($match ? "If" : "If-None"), $etag);
85 }
86
87 /*
88 * Add a Depth: header. Valid values are 0, 1 or infinity
89 *
90 * @param int $depth The depth, default to infinity
91 */
92 function SetDepth( $depth = '0' ) {
93 $this->headers[] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") );
94 }
95
101 function SetUserAgent( $user_agent = null ) {
102 if ( !isset($user_agent) ) $user_agent = $this->user_agent;
103 $this->user_agent = $user_agent;
104 }
105
111 function SetContentType( $type ) {
112 $this->headers[] = "Content-type: $type";
113 }
114
120 function ParseResponse( $response ) {
121 $pos = strpos($response, '<?xml');
122 if ($pos === false) {
123 $this->httpResponse = trim($response);
124 }
125 else {
126 $this->httpResponse = trim(substr($response, 0, $pos));
127 $this->xmlResponse = trim(substr($response, $pos));
128 }
129 }
130
136 function GetHttpRequest() {
137 return $this->httpRequest;
138 }
144 function GetHttpResponse() {
145 return $this->httpResponse;
146 }
152 function GetXmlRequest() {
153 return $this->xmlRequest;
154 }
160 function GetXmlResponse() {
161 return $this->xmlResponse;
162 }
163
171 function DoRequest( $relative_url = "" ) {
172 if(!defined("_FSOCK_TIMEOUT")){ define("_FSOCK_TIMEOUT", 10); }
173 $headers = array();
174
175 $headers[] = $this->requestMethod." ". $this->base_url . $relative_url . " HTTP/1.1";
176 $headers[] = "Authorization: Basic ".base64_encode($this->user .":". $this->pass );
177 $headers[] = "Host: ".$this->server .":".$this->port;
178
179 foreach( $this->headers as $ii => $head ) {
180 $headers[] = $head;
181 }
182 $headers[] = "Content-Length: " . strlen($this->body);
183 $headers[] = "User-Agent: " . $this->user_agent;
184 $headers[] = 'Connection: close';
185 $this->httpRequest = join("\r\n",$headers);
186 $this->xmlRequest = $this->body;
187
188 $fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT); //error handling?
189 if ( !(get_resource_type($fip) == 'stream') ) return false;
190 if ( !fwrite($fip, $this->httpRequest."\r\n\r\n".$this->body) ) { fclose($fip); return false; }
191 $rsp = "";
192 while( !feof($fip) ) { $rsp .= fgets($fip,8192); }
193 fclose($fip);
194
195 $this->headers = array(); // reset the headers array for our next request
196 $this->ParseResponse($rsp);
197 return $rsp;
198 }
199
200
208 function DoOptionsRequest( $relative_url = "" ) {
209 $this->requestMethod = "OPTIONS";
210 $this->body = "";
211 $headers = $this->DoRequest($relative_url);
212 $options_header = preg_replace( '/^.*Allow: ([a-z, ]+)\r?\n.*/is', '$1', $headers );
213 $options = array_flip( preg_split( '/[, ]+/', $options_header ));
214 return $options;
215 }
216
217
218
228 function DoXMLRequest( $request_method, $xml, $relative_url = '' ) {
229 $this->body = $xml;
230 $this->requestMethod = $request_method;
231 $this->SetContentType("text/xml");
232 return $this->DoRequest($relative_url);
233 }
234
235
236
242 function DoGETRequest( $relative_url ) {
243 $this->body = "";
244 $this->requestMethod = "GET";
245 return $this->DoRequest( $relative_url );
246 }
247
248
258 function DoPUTRequest( $relative_url, $icalendar, $etag = null ) {
259 $this->body = $icalendar;
260
261 $this->requestMethod = "PUT";
262 if ( $etag != null ) {
263 $this->SetMatch( ($etag != '*'), $etag );
264 }
265 $this->SetContentType("text/icalendar");
266 $headers = $this->DoRequest($relative_url);
267
272 $etag = preg_replace( '/^.*Etag: "?([^"\r\n]+)"?\r?\n.*/is', '$1', $headers );
273 return $etag;
274 }
275
276
285 function DoDELETERequest( $relative_url, $etag = null ) {
286 $this->body = "";
287
288 $this->requestMethod = "DELETE";
289 if ( $etag != null ) {
290 $this->SetMatch( true, $etag );
291 }
292 $this->DoRequest($relative_url);
293 return $this->resultcode;
294 }
295
296
310 function DoCalendarQuery( $filter, $relative_url = '' ) {
311
312 $xml = <<<EOXML
313<?xml version="1.0" encoding="utf-8" ?>
314<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
315 <D:prop>
316 <C:calendar-data/>
317 <D:getetag/>
318 </D:prop>$filter
319</C:calendar-query>
320EOXML;
321
322 $this->DoXMLRequest( 'REPORT', $xml, $relative_url );
323 $xml_parser = xml_parser_create_ns('UTF-8');
324 $this->xml_tags = array();
325 xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
326 xml_parse_into_struct( $xml_parser, $this->xmlResponse, $this->xml_tags );
327 xml_parser_free($xml_parser);
328
329 $report = array();
330 foreach( $this->xml_tags as $k => $v ) {
331 switch( $v['tag'] ) {
332 case 'DAV::RESPONSE':
333 if ( $v['type'] == 'open' ) {
334 $response = array();
335 }
336 elseif ( $v['type'] == 'close' ) {
337 $report[] = $response;
338 }
339 break;
340 case 'DAV::HREF':
341 $response['href'] = basename( $v['value'] );
342 break;
343 case 'DAV::GETETAG':
344 $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
345 break;
346 case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-DATA':
347 $response['data'] = $v['value'];
348 break;
349 }
350 }
351 return $report;
352 }
353
354
368 function GetEvents( $start = null, $finish = null, $relative_url = '' ) {
369 $filter = "";
370 if ( isset($start) && isset($finish) )
371 $range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
372 else
373 $range = '';
374
375 $filter = <<<EOFILTER
376 <C:filter>
377 <C:comp-filter name="VCALENDAR">
378 <C:comp-filter name="VEVENT">
379 $range
380 </C:comp-filter>
381 </C:comp-filter>
382 </C:filter>
383EOFILTER;
384
385 return $this->DoCalendarQuery($filter, $relative_url);
386 }
387
388
404 function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = "" ) {
405
406 if ( $start && $finish ) {
407$time_range = <<<EOTIME
408 <C:time-range start="$start" end="$finish"/>
409EOTIME;
410 }
411
412 // Warning! May contain traces of double negatives...
413 $neg_cancelled = ( $cancelled === true ? "no" : "yes" );
414 $neg_completed = ( $cancelled === true ? "no" : "yes" );
415
416 $filter = <<<EOFILTER
417 <C:filter>
418 <C:comp-filter name="VCALENDAR">
419 <C:comp-filter name="VTODO">
420 <C:prop-filter name="STATUS">
421 <C:text-match negate-condition="$neg_completed">COMPLETED</C:text-match>
422 </C:prop-filter>
423 <C:prop-filter name="STATUS">
424 <C:text-match negate-condition="$neg_cancelled">CANCELLED</C:text-match>
425 </C:prop-filter>$time_range
426 </C:comp-filter>
427 </C:comp-filter>
428 </C:filter>
429EOFILTER;
430
431 return $this->DoCalendarQuery($filter, $relative_url);
432 }
433
434
443 function GetEntryByUid( $uid, $relative_url = '' ) {
444 $filter = "";
445 if ( $uid ) {
446 $filter = <<<EOFILTER
447 <C:filter>
448 <C:comp-filter name="VCALENDAR">
449 <C:comp-filter name="VEVENT">
450 <C:prop-filter name="UID">
451 <C:text-match icollation="i;octet">$uid</C:text-match>
452 </C:prop-filter>
453 </C:comp-filter>
454 </C:comp-filter>
455 </C:filter>
456EOFILTER;
457 }
458
459 return $this->DoCalendarQuery($filter, $relative_url);
460 }
461
462
471 function GetEntryByHref( $href, $relative_url = '' ) {
472 return $this->DoGETRequest( $relative_url . $href );
473 }
474
475}
476
DoRequest( $url=null)
SetDepth( $depth='0')
SetMatch( $match, $etag=' *')
DoOptionsRequest( $relative_url="")
GetEntryByUid( $uid, $relative_url='')
GetTodos( $start, $finish, $completed=false, $cancelled=false, $relative_url="")
DoCalendarQuery( $filter, $url='')
DoXMLRequest( $request_method, $xml, $url=null)
ParseResponse( $response)
SetContentType( $type)
DoPUTRequest( $relative_url, $icalendar, $etag=null)
DoCalendarQuery( $filter, $relative_url='')
GetEvents( $start=null, $finish=null, $relative_url='')
GetEntryByHref( $href, $relative_url='')
DoXMLRequest( $request_method, $xml, $relative_url='')
DoDELETERequest( $relative_url, $etag=null)
SetUserAgent( $user_agent=null)
DoGETRequest( $relative_url)
__construct( $base_url, $user, $pass, $calendar='')
DoRequest( $relative_url="")