13 require_once(
'XMLDocument.php');
24 function __construct( $url, $displayname =
null, $getctag =
null ) {
26 $this->displayname = $displayname;
27 $this->getctag = $getctag;
30 function __toString() {
31 return(
'(URL: '.$this->url.
' Ctag: '.$this->getctag.
' Displayname: '.$this->displayname .
')'.
"\n" );
35 if(!defined(
"_FSOCK_TIMEOUT")){
36 define(
"_FSOCK_TIMEOUT", 10);
50 protected $base_url, $user, $pass, $entry, $protocol, $server, $port;
55 protected $principal_url;
60 protected $calendar_url;
65 protected $calendar_home_set;
70 protected $calendar_urls;
77 public $user_agent =
'DAViCalClient';
79 protected $headers = array();
81 protected $requestMethod =
"GET";
82 protected $httpRequest =
"";
83 protected $xmlRequest =
"";
84 protected $xmlResponse =
"";
85 protected $httpResponseCode = 0;
86 protected $httpResponseHeaders =
"";
87 protected $httpParsedHeaders;
88 protected $httpResponseBody =
"";
92 private $debug =
false;
101 function __construct( $base_url, $user, $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';
114 $this->protocol =
'tcp';
117 if ( $matches[4] !=
'' ) {
118 $this->port = intval($matches[4]);
122 trigger_error(
"Invalid URL: '".$base_url.
"'", E_USER_ERROR);
132 function SetDebug( $new_value ) {
133 $old_value = $this->debug;
137 $this->debug =
false;
149 function SetMatch( $match, $etag =
'*' ) {
150 $this->headers[
'match'] = sprintf(
"%s-Match: \"%s\"", ($match ?
"If" :
"If-None"), trim($etag,
'"'));
158 function SetDepth( $depth =
'0' ) {
159 $this->headers[
'depth'] =
'Depth: '. ($depth ==
'1' ?
"1" : ($depth ==
'infinity' ? $depth :
"0") );
167 function SetUserAgent( $user_agent =
null ) {
168 if ( !isset($user_agent) ) $user_agent = $this->user_agent;
169 $this->user_agent = $user_agent;
177 function SetContentType( $type ) {
178 $this->headers[
'content-type'] =
"Content-type: $type";
186 function SetCalendar( $url ) {
187 $this->calendar_url = $url;
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)) );
209 printf(
"\nXML Reponse:\n%s\n", $this->xmlResponse );
212 xml_parser_free($parser);
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);
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();
235 $this->httpParsedHeaders[$header_name][] = $cur_headers;
238 return $this->httpParsedHeaders;
246 function GetHttpRequest() {
247 return $this->httpRequest;
254 function GetResponseHeaders() {
255 return $this->httpResponseHeaders;
262 function GetResponseBody() {
263 return $this->httpResponseBody;
270 function GetXmlRequest() {
271 return $this->xmlRequest;
278 function GetXmlResponse() {
279 return $this->xmlResponse;
289 function DoRequest( $url =
null ) {
292 if ( !isset($url) ) $url = $this->base_url;
293 $this->request_url = $url;
294 $url = preg_replace(
'{^https?://[^/]+}',
'', $url);
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);
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 ) {
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);
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; }
324 while( !feof($fip) ) { $response .= fgets($fip,8192); }
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]);
332 $this->httpResponseCode = 0;
334 $this->headers = array();
335 $this->ParseResponse($this->httpResponseBody);
345 $chunks = $this->httpResponseBody;
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]);
356 $content .= substr($chunks,$pos,$bytes);
357 $chunks = substr($chunks,$pos + $bytes + 2);
366 $this->httpResponseBody = $content;
378 function DoOptionsRequest( $url =
null ) {
379 $this->requestMethod =
"OPTIONS";
381 $this->DoRequest($url);
382 $this->ParseResponseHeaders();
384 foreach( $this->httpParsedHeaders[
'Allow'] as $allow_header ) {
385 $allowed .= preg_replace(
'/^(Allow:)?\s+([a-z, ]+)\r?\n.*/is',
'$1,', $allow_header );
387 $options = array_flip( preg_split(
'/[, ]+/', trim($allowed,
', ') ));
402 function DoXMLRequest( $request_method, $xml, $url =
null ) {
404 $this->requestMethod = $request_method;
405 $this->SetContentType(
"text/xml");
406 return $this->DoRequest($url);
416 function DoGETRequest( $url ) {
418 $this->requestMethod =
"GET";
419 return $this->DoRequest( $url );
428 function DoHEADRequest( $url ) {
430 $this->requestMethod =
"HEAD";
431 return $this->DoRequest( $url );
444 function DoPUTRequest( $url, $icalendar, $etag =
null ) {
445 $this->body = $icalendar;
447 $this->requestMethod =
"PUT";
448 if ( $etag !=
null ) {
449 $this->SetMatch( ($etag !=
'*'), $etag );
451 $this->SetContentType(
'text/calendar; charset="utf-8"');
452 $this->DoRequest($url);
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 );
465 $this->httpRequest = $save_request;
466 $this->httpResponseHeaders = $save_response_headers;
480 function DoDELETERequest( $url, $etag =
null ) {
483 $this->requestMethod =
"DELETE";
484 if ( $etag !=
null ) {
485 $this->SetMatch(
true, $etag );
487 $this->DoRequest($url);
488 return $this->httpResponseCode;
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);
505 $this->body = $xml->Render(
'propfind',$prop );
507 $this->requestMethod =
"PROPFIND";
508 $this->SetContentType(
"text/xml");
509 $this->DoRequest($url);
510 return $this->GetXmlResponse();
519 function PrincipalURL( $url =
null ) {
521 $this->principal_url = $url;
523 return $this->principal_url;
532 function CalendarHomeSet( $urls =
null ) {
533 if ( isset($urls) ) {
534 if ( ! is_array($urls) ) $urls = array($urls);
535 $this->calendar_home_set = $urls;
537 return $this->calendar_home_set;
546 function CalendarUrls( $urls =
null ) {
547 if ( isset($urls) ) {
548 if ( ! is_array($urls) ) $urls = array($urls);
549 $this->calendar_urls = $urls;
551 return $this->calendar_urls;
560 function HrefValueInside( $tagname ) {
561 foreach( $this->xmltags[$tagname] AS $k => $v ) {
563 if ( $this->xmlnodes[$j][
'tag'] ==
'DAV::href' ) {
564 return rawurldecode($this->xmlnodes[$j][
'value']);
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' ) {
582 if ( $this->xmlnodes[$j][
'tag'] ==
'DAV::status' && $this->xmlnodes[$j][
'value'] !=
'HTTP/1.1 200 OK' )
return null;
585 if ( $j > 0 && isset($this->xmlnodes[$j][
'value']) ) {
587 return rawurldecode($this->xmlnodes[$j][
'value']);
591 if ( $this->debug ) printf(
"xmltags[$tagname] or xmltags[$tagname][$i] is not set\n");
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' );
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']);
623 function GetOKProps( $nodenum ) {
625 $level = $this->xmlnodes[$nodenum][
'level'];
627 while ( $this->xmlnodes[++$nodenum][
'level'] >= $level ) {
628 if ( $this->xmlnodes[$nodenum][
'tag'] ==
'DAV::propstat' ) {
629 if ( $this->xmlnodes[$nodenum][
'type'] ==
'open' ) {
634 if ( $status ==
'HTTP/1.1 200 OK' )
break;
637 elseif ( !isset($this->xmlnodes[$nodenum]) || !is_array($this->xmlnodes[$nodenum]) ) {
640 elseif ( $this->xmlnodes[$nodenum][
'tag'] ==
'DAV::status' ) {
641 $status = $this->xmlnodes[$nodenum][
'value'];
644 $props[] = $this->xmlnodes[$nodenum];
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);
670 return $this->PrincipalURL($principal_url);
679 function FindCalendarHome( $recursed=
false ) {
680 if ( !isset($this->principal_url) ) {
681 $this->FindPrincipal();
684 $this->DoPROPFINDRequest( $this->principal_url, array(
'urn:ietf:params:xml:ns:caldav:calendar-home-set'), 0);
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' ) {
692 if ( $this->xmlnodes[$v][
'tag'] ==
'DAV::href' && isset($this->xmlnodes[$v][
'value']) )
693 $calendar_home[] = rawurldecode($this->xmlnodes[$v][
'value']);
697 if ( !$recursed && count($calendar_home) < 1 ) {
698 $calendar_home = $this->FindCalendarHome(
true);
701 return $this->CalendarHomeSet($calendar_home);
708 function FindCalendars( $recursed=
false ) {
709 if ( !isset($this->calendar_home_set[0]) ) {
710 $this->FindCalendarHome();
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;
721 foreach( $this->xmltags[
'DAV::href'] AS $i => $hnode ) {
722 $href = rawurldecode($this->xmlnodes[$hnode][
'value']);
724 if ( !isset($calendar_urls[$href]) )
continue;
729 $ok_props = $this->GetOKProps($hnode);
730 foreach( $ok_props AS $v ) {
732 switch( $v[
'tag'] ) {
733 case 'http://calendarserver.org/ns/:getctag':
734 $calendar->getctag = $v[
'value'];
736 case 'DAV::displayname':
737 $calendar->displayname = $v[
'value'];
741 $calendars[] = $calendar;
745 return $this->CalendarUrls($calendars);
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']);
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'];
780 function GetCollectionETags( $url =
null ) {
781 if ( isset($url) ) $this->SetCalendar($url);
783 $this->DoPROPFINDRequest( $this->calendar_url, array(
'getetag'), 1);
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'];
800 function CalendarMultiget( $event_hrefs, $url =
null ) {
802 if ( isset($url) ) $this->SetCalendar($url);
805 foreach( $event_hrefs AS $k => $href ) {
806 $href = str_replace( rawurlencode(
'/'),
'/',rawurlencode($href));
807 $hrefs .=
'<href>'.$href.
'</href>';
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>
814 </C:calendar-multiget>
817 $this->requestMethod =
"REPORT";
818 $this->SetContentType(
"text/xml");
819 $this->DoRequest( $this->calendar_url );
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);
826 $events[$href] = $this->xmlnodes[$v][
'value'];
830 foreach( $event_hrefs AS $k => $href ) {
831 $this->DoGETRequest($href);
832 $events[$href] = $this->httpResponseBody;
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">
867 $this->requestMethod =
"REPORT";
868 $this->SetContentType(
"text/xml");
869 $this->DoRequest( $this->calendar_url );
872 foreach( $this->xmlnodes as $k => $v ) {
873 switch( $v[
'tag'] ) {
874 case 'DAV::response':
875 if ( $v[
'type'] ==
'open' ) {
878 elseif ( $v[
'type'] ==
'close' ) {
879 $report[] = $response;
883 $response[
'href'] = basename( rawurldecode($v[
'value']) );
886 $response[
'etag'] = preg_replace(
'/^"?([^"]+)"?/',
'$1', $v[
'value']);
888 case 'urn:ietf:params:xml:ns:caldav:calendar-data':
889 $response[
'data'] = $v[
'value'];
910 function GetEvents( $start =
null, $finish =
null, $relative_url =
'' ) {
912 if ( isset($start) && isset($finish) )
913 $range =
"<C:time-range start=\"$start\" end=\"$finish\"/>";
917 $filter = <<<EOFILTER
919 <C:comp-filter name=
"VCALENDAR">
920 <C:comp-filter name=
"VEVENT">
927 return $this->DoCalendarQuery($filter, $relative_url);
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"/>
955 $neg_cancelled = ( $cancelled ===
true ?
"no" :
"yes" );
956 $neg_completed = ( $cancelled ===
true ?
"no" :
"yes" );
958 $filter = <<<EOFILTER
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>
965 <C:prop-filter name=
"STATUS">
966 <C:text-match negate-condition=
"$neg_cancelled">CANCELLED</C:text-match>
967 </C:prop-filter>$time_range
973 return $this->DoCalendarQuery($filter, $relative_url);
986 function GetEntryByUid( $uid, $relative_url =
'', $component_type =
'VEVENT' ) {
989 $filter = <<<EOFILTER
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>
1002 return $this->DoCalendarQuery($filter, $relative_url);
1013 function GetEntryByHref( $href ) {
1014 $href = str_replace( rawurlencode(
'/'),
'/',rawurlencode($href));
1015 return $this->DoGETRequest( $href );