]>
Commit | Line | Data |
---|---|---|
a5eae6b7 MR |
1 | <?php |
2 | /** | |
3 | * A Class for connecting to a caldav server | |
4 | * | |
5 | * @package awl | |
6 | * removed curl - now using fsockopen | |
7 | * changed 2009 by Andres Obrero - Switzerland andres@obrero.ch | |
8 | * | |
9 | * @subpackage caldav | |
10 | * @author Andrew McMillan <debian@mcmillan.net.nz> | |
11 | * @copyright Andrew McMillan | |
12 | * @license http://gnu.org/copyleft/gpl.html GNU GPL v2 | |
13 | */ | |
14 | ||
15 | ||
16 | /** | |
17 | * A class for accessing DAViCal via CalDAV, as a client | |
18 | * | |
19 | * @package awl | |
20 | */ | |
21 | class CalDAVClient { | |
22 | /** | |
23 | * Server, username, password, calendar | |
24 | * | |
25 | * @var string | |
26 | */ | |
27 | var $base_url, $user, $pass, $calendar, $entry, $protocol, $server, $port; | |
28 | ||
29 | /** | |
30 | * The useragent which is send to the caldav server | |
31 | * | |
32 | * @var string | |
33 | */ | |
34 | var $user_agent = 'DAViCalClient'; | |
35 | ||
36 | var $headers = array(); | |
37 | var $body = ""; | |
38 | var $requestMethod = "GET"; | |
39 | var $httpRequest = ""; // for debugging http headers sent | |
40 | var $xmlRequest = ""; // for debugging xml sent | |
41 | var $httpResponse = ""; // for debugging http headers received | |
42 | var $xmlResponse = ""; // for debugging xml received | |
43 | ||
44 | /** | |
45 | * Constructor, initialises the class | |
46 | * | |
47 | * @param string $base_url The URL for the calendar server | |
48 | * @param string $user The name of the user logging in | |
49 | * @param string $pass The password for that user | |
50 | * @param string $calendar The name of the calendar (not currently used) | |
51 | */ | |
52 | function CalDAVClient( $base_url, $user, $pass, $calendar ) { | |
53 | $this->user = $user; | |
54 | $this->pass = $pass; | |
55 | $this->calendar = $calendar; | |
56 | $this->headers = array(); | |
57 | ||
58 | if ( preg_match( '#^(https?)://([a-z0-9.-]+)(:([0-9]+))?(/.*)$#', $base_url, $matches ) ) { | |
59 | $this->server = $matches[2]; | |
60 | $this->base_url = $matches[5]; | |
61 | if ( $matches[1] == 'https' ) { | |
62 | $this->protocol = 'ssl'; | |
63 | $this->port = 443; | |
64 | } | |
65 | else { | |
66 | $this->protocol = 'tcp'; | |
67 | $this->port = 80; | |
68 | } | |
69 | if ( $matches[4] != '' ) { | |
70 | $this->port = intval($matches[4]); | |
71 | } | |
72 | } | |
73 | else { | |
74 | trigger_error("Invalid URL: '".$base_url."'", E_USER_ERROR); | |
75 | } | |
76 | } | |
77 | ||
78 | /** | |
79 | * Adds an If-Match or If-None-Match header | |
80 | * | |
81 | * @param bool $match to Match or Not to Match, that is the question! | |
82 | * @param string $etag The etag to match / not match against. | |
83 | */ | |
84 | function SetMatch( $match, $etag = '*' ) { | |
85 | $this->headers[] = sprintf( "%s-Match: \"%s\"", ($match ? "If" : "If-None"), $etag); | |
86 | } | |
87 | ||
88 | /** | |
89 | * Add a Depth: header. Valid values are 1 or infinity | |
90 | * | |
91 | * @param int $depth The depth, default to infinity | |
92 | */ | |
93 | function SetDepth( $depth = 'infinity' ) { | |
94 | $this->headers[] = "Depth: ". ($depth == 1 ? "1" : "infinity" ); | |
95 | } | |
96 | ||
97 | /** | |
98 | * Add a Depth: header. Valid values are 1 or infinity | |
99 | * | |
100 | * @param int $depth The depth, default to infinity | |
101 | */ | |
102 | function SetUserAgent( $user_agent = null ) { | |
103 | if ( !isset($user_agent) ) $user_agent = $this->user_agent; | |
104 | $this->user_agent = $user_agent; | |
105 | } | |
106 | ||
107 | /** | |
108 | * Add a Content-type: header. | |
109 | * | |
110 | * @param int $type The content type | |
111 | */ | |
112 | function SetContentType( $type ) { | |
113 | $this->headers[] = "Content-type: $type"; | |
114 | } | |
115 | ||
116 | /** | |
117 | * Split response into httpResponse and xmlResponse | |
118 | * | |
119 | * @param string Response from server | |
120 | */ | |
121 | function ParseResponse( $response ) { | |
122 | $pos = strpos($response, '<?xml'); | |
123 | if ($pos == false) { | |
124 | $this->httpResponse = trim($response); | |
125 | } | |
126 | else { | |
127 | $this->httpResponse = trim(substr($response, 0, $pos)); | |
128 | $this->xmlResponse = trim(substr($response, $pos)); | |
129 | } | |
130 | } | |
131 | ||
132 | /** | |
133 | * Output http request headers | |
134 | * | |
135 | * @return HTTP headers | |
136 | */ | |
137 | function GetHttpRequest() { | |
138 | return $this->httpRequest; | |
139 | } | |
140 | /** | |
141 | * Output http response headers | |
142 | * | |
143 | * @return HTTP headers | |
144 | */ | |
145 | function GetHttpResponse() { | |
146 | return $this->httpResponse; | |
147 | } | |
148 | /** | |
149 | * Output xml request | |
150 | * | |
151 | * @return raw xml | |
152 | */ | |
153 | function GetXmlRequest() { | |
154 | return $this->xmlRequest; | |
155 | } | |
156 | /** | |
157 | * Output xml response | |
158 | * | |
159 | * @return raw xml | |
160 | */ | |
161 | function GetXmlResponse() { | |
162 | return $this->xmlResponse; | |
163 | } | |
164 | ||
165 | /** | |
166 | * Send a request to the server | |
167 | * | |
168 | * @param string $relative_url The URL to make the request to, relative to $base_url | |
169 | * | |
170 | * @return string The content of the response from the server | |
171 | */ | |
172 | function DoRequest( $relative_url = "" ) { | |
173 | if(!defined("_FSOCK_TIMEOUT")){ define("_FSOCK_TIMEOUT", 10); } | |
174 | $headers = array(); | |
175 | ||
176 | $headers[] = $this->requestMethod." ". $this->base_url . $relative_url . " HTTP/1.1"; | |
177 | $headers[] = "Authorization: Basic ".base64_encode($this->user .":". $this->pass ); | |
178 | $headers[] = "Host: ".$this->server .":".$this->port; | |
179 | ||
180 | foreach( $this->headers as $ii => $head ) { | |
181 | $headers[] = $head; | |
182 | } | |
183 | $headers[] = "Content-Length: " . strlen($this->body); | |
184 | $headers[] = "User-Agent: " . $this->user_agent; | |
185 | $headers[] = 'Connection: close'; | |
186 | $this->httpRequest = join("\r\n",$headers); | |
187 | $this->xmlRequest = $this->body; | |
188 | ||
189 | $fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT); //error handling? | |
190 | if ( !(get_resource_type($fip) == 'stream') ) return false; | |
191 | if ( !fwrite($fip, $this->httpRequest."\r\n\r\n".$this->body) ) { fclose($fip); return false; } | |
192 | $rsp = ""; | |
193 | while( !feof($fip) ) { $rsp .= fgets($fip,8192); } | |
194 | fclose($fip); | |
195 | ||
196 | $this->headers = array(); // reset the headers array for our next request | |
197 | $this->ParseResponse($rsp); | |
198 | return $rsp; | |
199 | } | |
200 | ||
201 | ||
202 | /** | |
203 | * Send an OPTIONS request to the server | |
204 | * | |
205 | * @param string $relative_url The URL to make the request to, relative to $base_url | |
206 | * | |
207 | * @return array The allowed options | |
208 | */ | |
209 | function DoOptionsRequest( $relative_url = "" ) { | |
210 | $this->requestMethod = "OPTIONS"; | |
211 | $this->body = ""; | |
212 | $headers = $this->DoRequest($relative_url); | |
213 | $options_header = preg_replace( '/^.*Allow: ([a-z, ]+)\r?\n.*/is', '$1', $headers ); | |
214 | $options = array_flip( preg_split( '/[, ]+/', $options_header )); | |
215 | return $options; | |
216 | } | |
217 | ||
218 | ||
219 | ||
220 | /** | |
221 | * Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR) | |
222 | * | |
223 | * @param string $method The method (PROPFIND, REPORT, etc) to use with the request | |
224 | * @param string $xml The XML to send along with the request | |
225 | * @param string $relative_url The URL to make the request to, relative to $base_url | |
226 | * | |
227 | * @return array An array of the allowed methods | |
228 | */ | |
229 | function DoXMLRequest( $request_method, $xml, $relative_url = '' ) { | |
230 | $this->body = $xml; | |
231 | $this->requestMethod = $request_method; | |
232 | $this->SetContentType("text/xml"); | |
233 | return $this->DoRequest($relative_url); | |
234 | } | |
235 | ||
236 | ||
237 | ||
238 | /** | |
239 | * Get a single item from the server. | |
240 | * | |
241 | * @param string $relative_url The part of the URL after the calendar | |
242 | */ | |
243 | function DoGETRequest( $relative_url ) { | |
244 | $this->body = ""; | |
245 | $this->requestMethod = "GET"; | |
246 | return $this->DoRequest( $relative_url ); | |
247 | } | |
248 | ||
249 | /** | |
250 | * PUT a text/icalendar resource, returning the etag | |
251 | * | |
252 | * @param string $relative_url The URL to make the request to, relative to $base_url | |
253 | * @param string $icalendar The iCalendar resource to send to the server | |
254 | * @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource. | |
255 | * | |
256 | * @return string The content of the response from the server | |
257 | */ | |
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/calendar"); | |
266 | $headers = $this->DoRequest($relative_url); | |
267 | ||
268 | /** | |
269 | * RSCDS will always return the real etag on PUT. Other CalDAV servers may need | |
270 | * more work, but we are assuming we are running against RSCDS in this case. | |
271 | */ | |
272 | preg_match('/^HTTP\/1\.[01]+\s+(\d+)\s+/i', $headers, $match); | |
273 | if ($match) { | |
274 | //print htmlentities($this->GetXmlResponse())."<br/>"; | |
275 | $xml = simplexml_load_string($this->GetXmlResponse()); | |
276 | $xml->registerXPathNamespace('e', 'DAV:'); | |
277 | $elems = $xml->xpath('//e:error'); | |
278 | $error = $elems[0]; | |
279 | switch ($match[1]) { | |
280 | case 412: | |
281 | //echo "$error<br/>"; | |
282 | preg_match('/^"?[^"]+"+([0-9a-zA-Z]+)"+.*"+([0-9a-zA-Z]+)"+/', $error, $m); | |
283 | //print_r($m); | |
284 | if ($m) { | |
285 | if (strcasecmp($m[1], $m[2]) !== 0) { | |
286 | $error = "Remote changes discovered.\n"; | |
287 | $error .= "Enter changes again after reload\n"; | |
288 | } | |
289 | } | |
290 | $etag = array($match[1] => "$error"); | |
291 | //print_r($etag); | |
292 | break; | |
293 | default: break; | |
294 | } | |
295 | } | |
296 | else | |
297 | $etag = preg_replace( '/^.*Etag: "?([^"\r\n]+)"?\r?\n.*/is', '$1', $headers ); | |
298 | return $etag; | |
299 | } | |
300 | ||
301 | ||
302 | /** | |
303 | * DELETE a text/icalendar resource | |
304 | * | |
305 | * @param string $relative_url The URL to make the request to, relative to $base_url | |
306 | * @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL. | |
307 | * | |
308 | * @return int The HTTP Result Code for the DELETE | |
309 | */ | |
310 | function DoDELETERequest( $relative_url, $etag = null ) { | |
311 | $this->body = ""; | |
312 | ||
313 | $this->requestMethod = "DELETE"; | |
314 | if ( $etag != null ) { | |
315 | $this->SetMatch( true, $etag ); | |
316 | } | |
317 | $this->DoRequest($relative_url); | |
318 | return $this->resultcode; | |
319 | } | |
320 | ||
321 | ||
322 | /** | |
323 | * Given XML for a calendar query, return an array of the events (/todos) in the | |
324 | * response. Each event in the array will have a 'href', 'etag' and '$response_type' | |
325 | * part, where the 'href' is relative to the calendar and the '$response_type' contains the | |
326 | * definition of the calendar data in iCalendar format. | |
327 | * | |
328 | * @param string $filter XML fragment which is the <filter> element of a calendar-query | |
329 | * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. | |
330 | * @param string $report_type Used as a name for the array element containing the calendar data. @deprecated | |
331 | * | |
332 | * @return array An array of the relative URLs, etags, and events from the server. Each element of the array will | |
333 | * be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied | |
334 | * etag (which only varies when the data changes) and the calendar data in iCalendar format. | |
335 | */ | |
336 | function DoCalendarQuery( $filter, $relative_url = '' ) { | |
337 | ||
338 | $xml = <<<EOXML | |
339 | <?xml version="1.0" encoding="utf-8" ?> | |
340 | <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> | |
341 | <D:prop> | |
342 | <C:calendar-data/> | |
343 | <D:getetag/> | |
344 | </D:prop> | |
345 | $filter | |
346 | </C:calendar-query> | |
347 | EOXML; | |
348 | ||
349 | $this->DoXMLRequest( 'REPORT', $xml, $relative_url ); | |
350 | $xml_parser = xml_parser_create_ns('UTF-8'); | |
351 | $this->xml_tags = array(); | |
352 | xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 ); | |
353 | xml_parse_into_struct( $xml_parser, $this->xmlResponse, $this->xml_tags ); | |
354 | xml_parser_free($xml_parser); | |
355 | ||
356 | $report = array(); | |
357 | foreach( $this->xml_tags as $k => $v ) { | |
358 | switch( $v['tag'] ) { | |
359 | case 'DAV::RESPONSE': | |
360 | if ( $v['type'] == 'open' ) { | |
361 | $response = array(); | |
362 | } | |
363 | elseif ( $v['type'] == 'close' ) { | |
364 | $report[] = $response; | |
365 | } | |
366 | break; | |
367 | case 'DAV::HREF': | |
368 | $response['href'] = basename( $v['value'] ); | |
369 | break; | |
370 | case 'DAV::GETETAG': | |
371 | $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']); | |
372 | break; | |
373 | case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-DATA': | |
374 | $response['data'] = $v['value']; | |
375 | break; | |
376 | } | |
377 | } | |
378 | return $report; | |
379 | } | |
380 | ||
381 | ||
382 | /** | |
383 | * Get the events in a range from $start to $finish. The dates should be in the | |
384 | * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an | |
385 | * array of event arrays. Each event array will have a 'href', 'etag' and 'event' | |
386 | * part, where the 'href' is relative to the calendar and the event contains the | |
387 | * definition of the event in iCalendar format. | |
388 | * | |
389 | * @param timestamp $start The start time for the period | |
390 | * @param timestamp $finish The finish time for the period | |
391 | * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. | |
392 | * | |
393 | * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery() | |
394 | */ | |
395 | function GetEvents( $start = null, $finish = null, $relative_url = '' ) { | |
396 | $filter = ""; | |
397 | if ( isset($start) && isset($finish) ) | |
398 | $range = "<C:time-range start=\"$start\" end=\"$finish\"/>"; | |
399 | else | |
400 | $range = ''; | |
401 | ||
402 | $filter = <<<EOFILTER | |
403 | <C:filter> | |
404 | <C:comp-filter name="VCALENDAR"> | |
405 | <C:comp-filter name="VEVENT"> | |
406 | $range | |
407 | </C:comp-filter> | |
408 | </C:comp-filter> | |
409 | </C:filter> | |
410 | EOFILTER; | |
411 | ||
412 | return $this->DoCalendarQuery($filter, $relative_url); | |
413 | } | |
414 | ||
415 | ||
416 | /** | |
417 | * Get the todo's in a range from $start to $finish. The dates should be in the | |
418 | * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an | |
419 | * array of event arrays. Each event array will have a 'href', 'etag' and 'event' | |
420 | * part, where the 'href' is relative to the calendar and the event contains the | |
421 | * definition of the event in iCalendar format. | |
422 | * | |
423 | * @param timestamp $start The start time for the period | |
424 | * @param timestamp $finish The finish time for the period | |
425 | * @param boolean $completed Whether to include completed tasks | |
426 | * @param boolean $cancelled Whether to include cancelled tasks | |
427 | * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. | |
428 | * | |
429 | * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery() | |
430 | */ | |
431 | function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = "" ) { | |
432 | ||
433 | if ( $start && $finish ) { | |
434 | $time_range = <<<EOTIME | |
435 | <C:time-range start="$start" end="$finish"/> | |
436 | EOTIME; | |
437 | } | |
438 | ||
439 | // Warning! May contain traces of double negatives... | |
440 | $neg_cancelled = ( $cancelled === true ? "no" : "yes" ); | |
441 | $neg_completed = ( $cancelled === true ? "no" : "yes" ); | |
442 | ||
443 | $filter = <<<EOFILTER | |
444 | <C:filter> | |
445 | <C:comp-filter name="VCALENDAR"> | |
446 | <C:comp-filter name="VTODO"> | |
447 | <C:prop-filter name="STATUS"> | |
448 | <C:text-match negate-condition="$neg_completed">COMPLETED</C:text-match> | |
449 | </C:prop-filter> | |
450 | <C:prop-filter name="STATUS"> | |
451 | <C:text-match negate-condition="$neg_cancelled">CANCELLED</C:text-match> | |
452 | </C:prop-filter>$time_range | |
453 | </C:comp-filter> | |
454 | </C:comp-filter> | |
455 | </C:filter> | |
456 | EOFILTER; | |
457 | ||
458 | return $this->DoCalendarQuery($filter, $relative_url); | |
459 | } | |
460 | ||
461 | ||
462 | /** | |
463 | * Get the calendar entry by UID | |
464 | * | |
465 | * @param uid | |
466 | * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. | |
467 | * | |
468 | * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery() | |
469 | */ | |
470 | function GetEntryByUid( $uid, $relative_url = '' ) { | |
471 | $filter = ""; | |
472 | if ( $uid ) { | |
473 | $filter = <<<EOFILTER | |
474 | <C:filter> | |
475 | <C:comp-filter name="VCALENDAR"> | |
476 | <C:comp-filter name="VEVENT"> | |
477 | <C:prop-filter name="UID"> | |
478 | <C:text-match icollation="i;octet">$uid</C:text-match> | |
479 | </C:prop-filter> | |
480 | </C:comp-filter> | |
481 | </C:comp-filter> | |
482 | </C:filter> | |
483 | EOFILTER; | |
484 | } | |
485 | ||
486 | return $this->DoCalendarQuery($filter, $relative_url); | |
487 | } | |
488 | ||
489 | ||
490 | /** | |
491 | * Get the calendar entry by HREF | |
492 | * | |
493 | * @param string $href The href from a call to GetEvents or GetTodos etc. | |
494 | * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''. | |
495 | * | |
496 | * @return string The iCalendar of the calendar entry | |
497 | */ | |
498 | function GetEntryByHref( $href, $relative_url = '' ) { | |
499 | return $this->DoGETRequest( $relative_url . $href ); | |
500 | } | |
501 | ||
502 | } | |
503 | ||
504 | //$cal = new CalDAVClient( "https://www.google.com/calendar/dav/[mail id]/events/", "uid", "pwd", "calendar" ); | |
505 | //$cal = new CalDAVClient( "http://calendar.datanom.net/caldav.php/[uid]/home", "uid", "pwd", "calendar" ); | |
506 | //$cal->SetDepth(1); | |
507 | /*$folder_xml = $cal->DoXMLRequest("PROPFIND", | |
508 | '<?xml version="1.0" encoding="utf-8" ?> | |
509 | <propfind xmlns="DAV:"> | |
510 | <prop> | |
511 | <getcontentlength/> | |
512 | <getcontenttype/> | |
513 | <resourcetype/> | |
514 | <getetag/> | |
515 | </prop> | |
516 | </propfind>' );*/ | |
517 | //print_r($folder_xml); | |
518 | ||
519 | ||
520 | /*$events = $cal->GetEvents("20100616T000000Z", "20100616T235959Z"); | |
521 | foreach ( $events AS $k => $event ) { | |
522 | print_r( $event['data'] ); | |
523 | }*/ | |
524 | ||
525 | ||
526 | /* | |
527 | print "Debug information\n"; | |
528 | print "Headers sent:\n".$cal->GetHttpRequest()."\n". | |
529 | "XML sent:\n".$cal->GetXmlRequest()."\n". | |
530 | "Headers received:\n".$cal->GetHttpResponse()."\n". | |
531 | "XML received:\n".$cal->GetXmlResponse()."\n"; | |
532 | */ |