caldav-client.php 19 KB

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