PageRenderTime 52ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/include/caldav-client-v2.php

https://github.com/MikeEvans/PHP-Push-2
PHP | 1056 lines | 550 code | 132 blank | 374 comment | 114 complexity | 41241acf541ab395d13ac0a18f13eceb MD5 | raw file
Possible License(s): AGPL-3.0
  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. */
  12. require_once('XMLDocument.php');
  13. /**
  14. * A class for holding basic calendar information
  15. * @package awl
  16. */
  17. class CalendarInfo {
  18. public $url, $displayname, $getctag;
  19. function __construct( $url, $displayname = null, $getctag = null ) {
  20. $this->url = $url;
  21. $this->displayname = $displayname;
  22. $this->getctag = $getctag;
  23. }
  24. function __toString() {
  25. return( '(URL: '.$this->url.' Ctag: '.$this->getctag.' Displayname: '.$this->displayname .')'. "\n" );
  26. }
  27. }
  28. /**
  29. * A class for accessing DAViCal via CalDAV, as a client
  30. *
  31. * @package awl
  32. */
  33. class CalDAVClient {
  34. /**
  35. * Server, username, password, calendar
  36. *
  37. * @var string
  38. */
  39. protected $base_url, $user, $pass, $entry, $protocol, $server, $port;
  40. /**
  41. * The principal-URL we're using
  42. */
  43. protected $principal_url;
  44. /**
  45. * The calendar-URL we're using
  46. */
  47. protected $calendar_url;
  48. /**
  49. * The calendar-home-set we're using
  50. */
  51. protected $calendar_home_set;
  52. /**
  53. * The calendar_urls we have discovered
  54. */
  55. protected $calendar_urls;
  56. /**
  57. * The useragent which is send to the caldav server
  58. *
  59. * @var string
  60. */
  61. public $user_agent = 'DAViCalClient';
  62. protected $headers = array();
  63. protected $body = "";
  64. protected $requestMethod = "GET";
  65. protected $httpRequest = ""; // for debugging http headers sent
  66. protected $xmlRequest = ""; // for debugging xml sent
  67. protected $xmlResponse = ""; // xml received
  68. protected $httpResponseCode = 0; // http response code
  69. protected $httpResponseHeaders = "";
  70. protected $httpResponseBody = "";
  71. protected $parser; // our XML parser object
  72. private $debug = false; // Whether we are debugging
  73. /**
  74. * Constructor, initialises the class
  75. *
  76. * @param string $base_url The URL for the calendar server
  77. * @param string $user The name of the user logging in
  78. * @param string $pass The password for that user
  79. */
  80. function __construct( $base_url, $user, $pass ) {
  81. $this->user = $user;
  82. $this->pass = $pass;
  83. $this->headers = array();
  84. if ( preg_match( '#^(https?)://([a-z0-9.-]+)(:([0-9]+))?(/.*)$#', $base_url, $matches ) ) {
  85. $this->server = $matches[2];
  86. $this->base_url = $matches[5];
  87. if ( $matches[1] == 'https' ) {
  88. $this->protocol = 'ssl';
  89. $this->port = 443;
  90. }
  91. else {
  92. $this->protocol = 'tcp';
  93. $this->port = 80;
  94. }
  95. if ( $matches[4] != '' ) {
  96. $this->port = intval($matches[4]);
  97. }
  98. }
  99. else {
  100. trigger_error("Invalid URL: '".$base_url."'", E_USER_ERROR);
  101. }
  102. }
  103. /**
  104. * Call this to enable / disable debugging. It will return the prior value of the debugging flag.
  105. * @param boolean $new_value The new value for debugging.
  106. * @return boolean The previous value, in case you want to restore it later.
  107. */
  108. function SetDebug( $new_value ) {
  109. $old_value = $this->debug;
  110. if ( $new_value )
  111. $this->debug = true;
  112. else
  113. $this->debug = false;
  114. return $old_value;
  115. }
  116. /**
  117. * Adds an If-Match or If-None-Match header
  118. *
  119. * @param bool $match to Match or Not to Match, that is the question!
  120. * @param string $etag The etag to match / not match against.
  121. */
  122. function SetMatch( $match, $etag = '*' ) {
  123. $this->headers['match'] = sprintf( "%s-Match: \"%s\"", ($match ? "If" : "If-None"), trim($etag,'"'));
  124. }
  125. /**
  126. * Add a Depth: header. Valid values are 0, 1 or infinity
  127. *
  128. * @param int $depth The depth, default to infinity
  129. */
  130. function SetDepth( $depth = '0' ) {
  131. $this->headers['depth'] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") );
  132. }
  133. /**
  134. * Add a Depth: header. Valid values are 1 or infinity
  135. *
  136. * @param int $depth The depth, default to infinity
  137. */
  138. function SetUserAgent( $user_agent = null ) {
  139. if ( !isset($user_agent) ) $user_agent = $this->user_agent;
  140. $this->user_agent = $user_agent;
  141. }
  142. /**
  143. * Add a Content-type: header.
  144. *
  145. * @param string $type The content type
  146. */
  147. function SetContentType( $type ) {
  148. $this->headers['content-type'] = "Content-type: $type";
  149. }
  150. /**
  151. * Set the calendar_url we will be using for a while.
  152. *
  153. * @param string $url The calendar_url
  154. */
  155. function SetCalendar( $url ) {
  156. $this->calendar_url = $url;
  157. }
  158. /**
  159. * Split response into httpResponse and xmlResponse
  160. *
  161. * @param string Response from server
  162. */
  163. function ParseResponse( $response ) {
  164. $pos = strpos($response, '<?xml');
  165. if ($pos !== false) {
  166. $this->xmlResponse = trim(substr($response, $pos));
  167. $this->xmlResponse = preg_replace('{>[^>]*$}s', '>',$this->xmlResponse );
  168. $parser = xml_parser_create_ns('UTF-8');
  169. xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 );
  170. xml_parser_set_option ( $parser, XML_OPTION_CASE_FOLDING, 0 );
  171. if ( xml_parse_into_struct( $parser, $this->xmlResponse, $this->xmlnodes, $this->xmltags ) === 0 ) {
  172. printf( "XML parsing error: %s - %s\n", xml_get_error_code($parser), xml_error_string(xml_get_error_code($parser)) );
  173. // debug_print_backtrace();
  174. // echo "\nNodes array............................................................\n"; print_r( $this->xmlnodes );
  175. // echo "\nTags array............................................................\n"; print_r( $this->xmltags );
  176. printf( "\nXML Reponse:\n%s\n", $this->xmlResponse );
  177. }
  178. xml_parser_free($parser);
  179. }
  180. }
  181. /**
  182. * Output http request headers
  183. *
  184. * @return HTTP headers
  185. */
  186. function GetHttpRequest() {
  187. return $this->httpRequest;
  188. }
  189. /**
  190. * Output http response headers
  191. *
  192. * @return HTTP headers
  193. */
  194. function GetResponseHeaders() {
  195. return $this->httpResponseHeaders;
  196. }
  197. /**
  198. * Output http response body
  199. *
  200. * @return HTTP body
  201. */
  202. function GetResponseBody() {
  203. return $this->httpResponseBody;
  204. }
  205. /**
  206. * Output xml request
  207. *
  208. * @return raw xml
  209. */
  210. function GetXmlRequest() {
  211. return $this->xmlRequest;
  212. }
  213. /**
  214. * Output xml response
  215. *
  216. * @return raw xml
  217. */
  218. function GetXmlResponse() {
  219. return $this->xmlResponse;
  220. }
  221. /**
  222. * Send a request to the server
  223. *
  224. * @param string $url The URL to make the request to
  225. *
  226. * @return string The content of the response from the server
  227. */
  228. function DoRequest( $url = null ) {
  229. if(!defined("_FSOCK_TIMEOUT")){ define("_FSOCK_TIMEOUT", 10); }
  230. $headers = array();
  231. if ( !isset($url) ) $url = $this->base_url;
  232. $this->request_url = $url;
  233. $url = preg_replace('{^https?://[^/]+}', '', $url);
  234. // URLencode if it isn't already
  235. if ( preg_match( '{[^%?&=+,.-_/a-z0-9]}', $url ) ) {
  236. $url = str_replace(rawurlencode('/'),'/',rawurlencode($url));
  237. $url = str_replace(rawurlencode('?'),'?',$url);
  238. $url = str_replace(rawurlencode('&'),'&',$url);
  239. $url = str_replace(rawurlencode('='),'=',$url);
  240. $url = str_replace(rawurlencode('+'),'+',$url);
  241. $url = str_replace(rawurlencode(','),',',$url);
  242. }
  243. $headers[] = $this->requestMethod." ". $url . " HTTP/1.1";
  244. $headers[] = "Authorization: Basic ".base64_encode($this->user .":". $this->pass );
  245. $headers[] = "Host: ".$this->server .":".$this->port;
  246. if ( !isset($this->headers['content-type']) ) $this->headers['content-type'] = "Content-type: text/plain";
  247. foreach( $this->headers as $ii => $head ) {
  248. $headers[] = $head;
  249. }
  250. $headers[] = "Content-Length: " . strlen($this->body);
  251. $headers[] = "User-Agent: " . $this->user_agent;
  252. $headers[] = 'Connection: close';
  253. $this->httpRequest = join("\r\n",$headers);
  254. $this->xmlRequest = $this->body;
  255. $this->xmlResponse = '';
  256. $fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT); //error handling?
  257. if ( !(get_resource_type($fip) == 'stream') ) return false;
  258. if ( !fwrite($fip, $this->httpRequest."\r\n\r\n".$this->body) ) { fclose($fip); return false; }
  259. $response = "";
  260. while( !feof($fip) ) { $response .= fgets($fip,8192); }
  261. fclose($fip);
  262. list( $this->httpResponseHeaders, $this->httpResponseBody ) = preg_split( '{\r?\n\r?\n}s', $response, 2 );
  263. if ( preg_match( '{Transfer-Encoding: chunked}i', $this->httpResponseHeaders ) ) $this->Unchunk();
  264. if ( preg_match('/HTTP\/\d\.\d (\d{3})/', $this->httpResponseHeaders, $status) )
  265. $this->httpResponseCode = intval($status[1]);
  266. else
  267. $this->httpResponseCode = 0;
  268. $this->headers = array(); // reset the headers array for our next request
  269. $this->ParseResponse($this->httpResponseBody);
  270. return $response;
  271. }
  272. /**
  273. * Unchunk a chunked response
  274. */
  275. function Unchunk() {
  276. $content = '';
  277. $chunks = $this->httpResponseBody;
  278. // printf( "\n================================\n%s\n================================\n", $chunks );
  279. do {
  280. $bytes = 0;
  281. if ( preg_match('{^((\r\n)?\s*([ 0-9a-fA-F]+)(;[^\n]*)?\r?\n)}', $chunks, $matches ) ) {
  282. $octets = $matches[3];
  283. $bytes = hexdec($octets);
  284. $pos = strlen($matches[1]);
  285. // printf( "Chunk size 0x%s (%d)\n", $octets, $bytes );
  286. if ( $bytes > 0 ) {
  287. // printf( "---------------------------------\n%s\n---------------------------------\n", substr($chunks,$pos,$bytes) );
  288. $content .= substr($chunks,$pos,$bytes);
  289. $chunks = substr($chunks,$pos + $bytes + 2);
  290. // printf( "+++++++++++++++++++++++++++++++++\n%s\n+++++++++++++++++++++++++++++++++\n", $chunks );
  291. }
  292. }
  293. else {
  294. $content .= $chunks;
  295. }
  296. }
  297. while( $bytes > 0 );
  298. $this->httpResponseBody = $content;
  299. // printf( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n%s\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", $content );
  300. }
  301. /**
  302. * Send an OPTIONS request to the server
  303. *
  304. * @param string $url The URL to make the request to
  305. *
  306. * @return array The allowed options
  307. */
  308. function DoOptionsRequest( $url = null ) {
  309. $this->requestMethod = "OPTIONS";
  310. $this->body = "";
  311. $headers = $this->DoRequest($url);
  312. $options_header = preg_replace( '/^.*Allow: ([a-z, ]+)\r?\n.*/is', '$1', $headers );
  313. $options = array_flip( preg_split( '/[, ]+/', $options_header ));
  314. return $options;
  315. }
  316. /**
  317. * Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR)
  318. *
  319. * @param string $method The method (PROPFIND, REPORT, etc) to use with the request
  320. * @param string $xml The XML to send along with the request
  321. * @param string $url The URL to make the request to
  322. *
  323. * @return array An array of the allowed methods
  324. */
  325. function DoXMLRequest( $request_method, $xml, $url = null ) {
  326. $this->body = $xml;
  327. $this->requestMethod = $request_method;
  328. $this->SetContentType("text/xml");
  329. return $this->DoRequest($url);
  330. }
  331. /**
  332. * Get a single item from the server.
  333. *
  334. * @param string $url The URL to GET
  335. */
  336. function DoGETRequest( $url ) {
  337. $this->body = "";
  338. $this->requestMethod = "GET";
  339. return $this->DoRequest( $url );
  340. }
  341. /**
  342. * Get the HEAD of a single item from the server.
  343. *
  344. * @param string $url The URL to HEAD
  345. */
  346. function DoHEADRequest( $url ) {
  347. $this->body = "";
  348. $this->requestMethod = "HEAD";
  349. return $this->DoRequest( $url );
  350. }
  351. /**
  352. * PUT a text/icalendar resource, returning the etag
  353. *
  354. * @param string $url The URL to make the request to
  355. * @param string $icalendar The iCalendar resource to send to the server
  356. * @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource.
  357. *
  358. * @return string The content of the response from the server
  359. */
  360. function DoPUTRequest( $url, $icalendar, $etag = null ) {
  361. $this->body = $icalendar;
  362. $this->requestMethod = "PUT";
  363. if ( $etag != null ) {
  364. $this->SetMatch( ($etag != '*'), $etag );
  365. }
  366. $this->SetContentType('text/calendar; encoding="utf-8"');
  367. $this->DoRequest($url);
  368. $etag = null;
  369. if ( preg_match( '{^ETag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1];
  370. if ( !isset($etag) || $etag == '' ) {
  371. if ( $this->debug ) printf( "No etag in:\n%s\n", $this->httpResponseHeaders );
  372. $save_request = $this->httpRequest;
  373. $save_response_headers = $this->httpResponseHeaders;
  374. $this->DoHEADRequest( $url );
  375. if ( preg_match( '{^Etag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1];
  376. if ( !isset($etag) || $etag == '' ) {
  377. if ( $this->debug ) printf( "Still No etag in:\n%s\n", $this->httpResponseHeaders );
  378. }
  379. $this->httpRequest = $save_request;
  380. $this->httpResponseHeaders = $save_response_headers;
  381. }
  382. return $etag;
  383. }
  384. /**
  385. * DELETE a text/icalendar resource
  386. *
  387. * @param string $url The URL to make the request to
  388. * @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL.
  389. *
  390. * @return int The HTTP Result Code for the DELETE
  391. */
  392. function DoDELETERequest( $url, $etag = null ) {
  393. $this->body = "";
  394. $this->requestMethod = "DELETE";
  395. if ( $etag != null ) {
  396. $this->SetMatch( true, $etag );
  397. }
  398. $this->DoRequest($url);
  399. return $this->httpResponseCode;
  400. }
  401. /**
  402. * Get a single item from the server.
  403. *
  404. * @param string $url The URL to PROPFIND on
  405. */
  406. function DoPROPFINDRequest( $url, $props, $depth = 0 ) {
  407. $this->SetDepth($depth);
  408. $xml = new XMLDocument( array( 'DAV:' => '', 'urn:ietf:params:xml:ns:caldav' => 'C' ) );
  409. $prop = new XMLElement('prop');
  410. foreach( $props AS $v ) {
  411. $xml->NSElement($prop,$v);
  412. }
  413. $this->body = $xml->Render('propfind',$prop );
  414. $this->requestMethod = "PROPFIND";
  415. $this->SetContentType("text/xml");
  416. $this->DoRequest($url);
  417. return $this->GetXmlResponse();
  418. }
  419. /**
  420. * Get/Set the Principal URL
  421. *
  422. * @param $url string The Principal URL to set
  423. */
  424. function PrincipalURL( $url = null ) {
  425. if ( isset($url) ) {
  426. $this->principal_url = $url;
  427. }
  428. return $this->principal_url;
  429. }
  430. /**
  431. * Get/Set the calendar-home-set URL
  432. *
  433. * @param $url array of string The calendar-home-set URLs to set
  434. */
  435. function CalendarHomeSet( $urls = null ) {
  436. if ( isset($urls) ) {
  437. if ( ! is_array($urls) ) $urls = array($urls);
  438. $this->calendar_home_set = $urls;
  439. }
  440. return $this->calendar_home_set;
  441. }
  442. /**
  443. * Get/Set the calendar-home-set URL
  444. *
  445. * @param $urls array of string The calendar URLs to set
  446. */
  447. function CalendarUrls( $urls = null ) {
  448. if ( isset($urls) ) {
  449. if ( ! is_array($urls) ) $urls = array($urls);
  450. $this->calendar_urls = $urls;
  451. }
  452. return $this->calendar_urls;
  453. }
  454. /**
  455. * Return the first occurrence of an href inside the named tag.
  456. *
  457. * @param string $tagname The tag name to find the href inside of
  458. */
  459. function HrefValueInside( $tagname ) {
  460. foreach( $this->xmltags[$tagname] AS $k => $v ) {
  461. $j = $v + 1;
  462. if ( $this->xmlnodes[$j]['tag'] == 'DAV::href' ) {
  463. return rawurldecode($this->xmlnodes[$j]['value']);
  464. }
  465. }
  466. return null;
  467. }
  468. /**
  469. * Return the href containing this property. Except only if it's inside a status != 200
  470. *
  471. * @param string $tagname The tag name of the property to find the href for
  472. * @param integer $which Which instance of the tag should we use
  473. */
  474. function HrefForProp( $tagname, $i = 0 ) {
  475. if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
  476. $j = $this->xmltags[$tagname][$i];
  477. while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' ) {
  478. // printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
  479. if ( $this->xmlnodes[$j]['tag'] == 'DAV::status' && $this->xmlnodes[$j]['value'] != 'HTTP/1.1 200 OK' ) return null;
  480. }
  481. // printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
  482. if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
  483. // printf( "Value[$j]: %s\n", $this->xmlnodes[$j]['value']);
  484. return rawurldecode($this->xmlnodes[$j]['value']);
  485. }
  486. }
  487. else {
  488. if ( $this->debug ) printf( "xmltags[$tagname] or xmltags[$tagname][$i] is not set\n");
  489. }
  490. return null;
  491. }
  492. /**
  493. * Return the href which has a resourcetype of the specified type
  494. *
  495. * @param string $tagname The tag name of the resourcetype to find the href for
  496. * @param integer $which Which instance of the tag should we use
  497. */
  498. function HrefForResourcetype( $tagname, $i = 0 ) {
  499. if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
  500. $j = $this->xmltags[$tagname][$i];
  501. while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::resourcetype' );
  502. if ( $j > 0 ) {
  503. while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' );
  504. if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
  505. return rawurldecode($this->xmlnodes[$j]['value']);
  506. }
  507. }
  508. }
  509. return null;
  510. }
  511. /**
  512. * Return the <prop> ... </prop> of a propstat where the status is OK
  513. *
  514. * @param string $nodenum The node number in the xmlnodes which is the href
  515. */
  516. function GetOKProps( $nodenum ) {
  517. $props = null;
  518. $level = $this->xmlnodes[$nodenum]['level'];
  519. $status = '';
  520. while ( $this->xmlnodes[++$nodenum]['level'] >= $level ) {
  521. if ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::propstat' ) {
  522. if ( $this->xmlnodes[$nodenum]['type'] == 'open' ) {
  523. $props = array();
  524. $status = '';
  525. }
  526. else {
  527. if ( $status == 'HTTP/1.1 200 OK' ) break;
  528. }
  529. }
  530. elseif ( !isset($this->xmlnodes[$nodenum]) || !is_array($this->xmlnodes[$nodenum]) ) {
  531. break;
  532. }
  533. elseif ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::status' ) {
  534. $status = $this->xmlnodes[$nodenum]['value'];
  535. }
  536. else {
  537. $props[] = $this->xmlnodes[$nodenum];
  538. }
  539. }
  540. return $props;
  541. }
  542. /**
  543. * Attack the given URL in an attempt to find a principal URL
  544. *
  545. * @param string $url The URL to find the principal-URL from
  546. */
  547. function FindPrincipal( $url=null ) {
  548. $xml = $this->DoPROPFINDRequest( $url, array('resourcetype', 'current-user-principal', 'owner', 'principal-URL',
  549. 'urn:ietf:params:xml:ns:caldav:calendar-home-set'), 1);
  550. $principal_url = $this->HrefForProp('DAV::principal');
  551. if ( !isset($principal_url) ) {
  552. foreach( array('DAV::current-user-principal', 'DAV::principal-URL', 'DAV::owner') AS $href ) {
  553. if ( !isset($principal_url) ) {
  554. $principal_url = $this->HrefValueInside($href);
  555. }
  556. }
  557. }
  558. return $this->PrincipalURL($principal_url);
  559. }
  560. /**
  561. * Attack the given URL in an attempt to find a principal URL
  562. *
  563. * @param string $url The URL to find the calendar-home-set from
  564. */
  565. function FindCalendarHome( $recursed=false ) {
  566. if ( !isset($this->principal_url) ) {
  567. $this->FindPrincipal();
  568. }
  569. if ( $recursed ) {
  570. $this->DoPROPFINDRequest( $this->principal_url, array('urn:ietf:params:xml:ns:caldav:calendar-home-set'), 0);
  571. }
  572. $calendar_home = array();
  573. foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-home-set'] AS $k => $v ) {
  574. if ( $this->xmlnodes[$v]['type'] != 'open' ) continue;
  575. while( $this->xmlnodes[++$v]['type'] != 'close' && $this->xmlnodes[$v]['tag'] != 'urn:ietf:params:xml:ns:caldav:calendar-home-set' ) {
  576. // printf( "Tag: '%s' = '%s'\n", $this->xmlnodes[$v]['tag'], $this->xmlnodes[$v]['value']);
  577. if ( $this->xmlnodes[$v]['tag'] == 'DAV::href' && isset($this->xmlnodes[$v]['value']) )
  578. $calendar_home[] = rawurldecode($this->xmlnodes[$v]['value']);
  579. }
  580. }
  581. if ( !$recursed && count($calendar_home) < 1 ) {
  582. $calendar_home = $this->FindCalendarHome(true);
  583. }
  584. return $this->CalendarHomeSet($calendar_home);
  585. }
  586. /**
  587. * Find the calendars, from the calendar_home_set
  588. */
  589. function FindCalendars( $recursed=false ) {
  590. if ( !isset($this->calendar_home_set[0]) ) {
  591. $this->FindCalendarHome();
  592. }
  593. $this->DoPROPFINDRequest( $this->calendar_home_set[0], array('resourcetype','displayname','http://calendarserver.org/ns/:getctag'), 1);
  594. $calendars = array();
  595. if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar']) ) {
  596. $calendar_urls = array();
  597. foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar'] AS $k => $v ) {
  598. $calendar_urls[$this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar', $k)] = 1;
  599. }
  600. foreach( $this->xmltags['DAV::href'] AS $i => $hnode ) {
  601. $href = rawurldecode($this->xmlnodes[$hnode]['value']);
  602. if ( !isset($calendar_urls[$href]) ) continue;
  603. // printf("Seems '%s' is a calendar.\n", $href );
  604. $calendar = new CalendarInfo($href);
  605. $ok_props = $this->GetOKProps($hnode);
  606. foreach( $ok_props AS $v ) {
  607. // printf("Looking at: %s[%s]\n", $href, $v['tag'] );
  608. switch( $v['tag'] ) {
  609. case 'http://calendarserver.org/ns/:getctag':
  610. $calendar->getctag = $v['value'];
  611. break;
  612. case 'DAV::displayname':
  613. $calendar->displayname = $v['value'];
  614. break;
  615. }
  616. }
  617. $calendars[] = $calendar;
  618. }
  619. }
  620. return $this->CalendarUrls($calendars);
  621. }
  622. /**
  623. * Find the calendars, from the calendar_home_set
  624. */
  625. function GetCalendarDetails( $url = null ) {
  626. if ( isset($url) ) $this->SetCalendar($url);
  627. $calendar_properties = array( 'resourcetype', 'displayname', 'http://calendarserver.org/ns/:getctag', 'urn:ietf:params:xml:ns:caldav:calendar-timezone', 'supported-report-set' );
  628. $this->DoPROPFINDRequest( $this->calendar_url, $calendar_properties, 0);
  629. $hnode = $this->xmltags['DAV::href'][0];
  630. $href = rawurldecode($this->xmlnodes[$hnode]['value']);
  631. $calendar = new CalendarInfo($href);
  632. $ok_props = $this->GetOKProps($hnode);
  633. foreach( $ok_props AS $k => $v ) {
  634. $name = preg_replace( '{^.*:}', '', $v['tag'] );
  635. if ( isset($v['value'] ) ) {
  636. $calendar->{$name} = $v['value'];
  637. }
  638. /* else {
  639. printf( "Calendar property '%s' has no text content\n", $v['tag'] );
  640. }*/
  641. }
  642. return $calendar;
  643. }
  644. /**
  645. * Get all etags for a calendar
  646. */
  647. function GetCollectionETags( $url = null ) {
  648. if ( isset($url) ) $this->SetCalendar($url);
  649. $this->DoPROPFINDRequest( $this->calendar_url, array('getetag'), 1);
  650. $etags = array();
  651. if ( isset($this->xmltags['DAV::getetag']) ) {
  652. foreach( $this->xmltags['DAV::getetag'] AS $k => $v ) {
  653. $href = $this->HrefForProp('DAV::getetag', $k);
  654. if ( isset($href) && isset($this->xmlnodes[$v]['value']) ) $etags[$href] = $this->xmlnodes[$v]['value'];
  655. }
  656. }
  657. return $etags;
  658. }
  659. /**
  660. * Get a bunch of events for a calendar with a calendar-multiget report
  661. */
  662. function CalendarMultiget( $event_hrefs, $url = null ) {
  663. if ( isset($url) ) $this->SetCalendar($url);
  664. $hrefs = '';
  665. foreach( $event_hrefs AS $k => $href ) {
  666. $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
  667. $hrefs .= '<href>'.$href.'</href>';
  668. }
  669. $this->body = <<<EOXML
  670. <?xml version="1.0" encoding="utf-8" ?>
  671. <C:calendar-multiget xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  672. <prop><getetag/><C:calendar-data/></prop>
  673. $hrefs
  674. </C:calendar-multiget>
  675. EOXML;
  676. $this->requestMethod = "REPORT";
  677. $this->SetContentType("text/xml");
  678. $this->DoRequest( $this->calendar_url );
  679. $events = array();
  680. if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data']) ) {
  681. foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data'] AS $k => $v ) {
  682. $href = $this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar-data', $k);
  683. // echo "Calendar-data:\n"; print_r($this->xmlnodes[$v]);
  684. $events[$href] = $this->xmlnodes[$v]['value'];
  685. }
  686. }
  687. else {
  688. foreach( $event_hrefs AS $k => $href ) {
  689. $this->DoGETRequest($href);
  690. $events[$href] = $this->httpResponseBody;
  691. }
  692. }
  693. return $events;
  694. }
  695. /**
  696. * Given XML for a calendar query, return an array of the events (/todos) in the
  697. * response. Each event in the array will have a 'href', 'etag' and '$response_type'
  698. * part, where the 'href' is relative to the calendar and the '$response_type' contains the
  699. * definition of the calendar data in iCalendar format.
  700. *
  701. * @param string $filter XML fragment which is the <filter> element of a calendar-query
  702. * @param string $url The URL of the calendar, or empty/null to use the 'current' calendar_url
  703. *
  704. * @return array An array of the relative URLs, etags, and events from the server. Each element of the array will
  705. * be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied
  706. * etag (which only varies when the data changes) and the calendar data in iCalendar format.
  707. */
  708. function DoCalendarQuery( $filter, $url = '' ) {
  709. if ( !empty($url) ) $this->SetCalendar($url);
  710. $this->body = <<<EOXML
  711. <?xml version="1.0" encoding="utf-8" ?>
  712. <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  713. <D:prop>
  714. <C:calendar-data/>
  715. <D:getetag/>
  716. </D:prop>$filter
  717. </C:calendar-query>
  718. EOXML;
  719. $this->requestMethod = "REPORT";
  720. $this->SetContentType("text/xml");
  721. $this->DoRequest( $this->calendar_url );
  722. $report = array();
  723. foreach( $this->xmlnodes as $k => $v ) {
  724. switch( $v['tag'] ) {
  725. case 'DAV::response':
  726. if ( $v['type'] == 'open' ) {
  727. $response = array();
  728. }
  729. elseif ( $v['type'] == 'close' ) {
  730. $report[] = $response;
  731. }
  732. break;
  733. case 'DAV::href':
  734. $response['href'] = basename( rawurldecode($v['value']) );
  735. break;
  736. case 'DAV::getetag':
  737. $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
  738. break;
  739. case 'urn:ietf:params:xml:ns:caldav:calendar-data':
  740. $response['data'] = $v['value'];
  741. break;
  742. }
  743. }
  744. return $report;
  745. }
  746. /**
  747. * Get the events in a range from $start to $finish. The dates should be in the
  748. * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
  749. * array of event arrays. Each event array will have a 'href', 'etag' and 'event'
  750. * part, where the 'href' is relative to the calendar and the event contains the
  751. * definition of the event in iCalendar format.
  752. *
  753. * @param timestamp $start The start time for the period
  754. * @param timestamp $finish The finish time for the period
  755. * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
  756. *
  757. * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
  758. */
  759. function GetEvents( $start = null, $finish = null, $relative_url = '' ) {
  760. $filter = "";
  761. if ( isset($start) && isset($finish) )
  762. $range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
  763. else
  764. $range = '';
  765. $filter = <<<EOFILTER
  766. <C:filter>
  767. <C:comp-filter name="VCALENDAR">
  768. <C:comp-filter name="VEVENT">
  769. $range
  770. </C:comp-filter>
  771. </C:comp-filter>
  772. </C:filter>
  773. EOFILTER;
  774. return $this->DoCalendarQuery($filter, $relative_url);
  775. }
  776. /**
  777. * Get the todo's in a range from $start to $finish. The dates should be in the
  778. * format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
  779. * array of event arrays. Each event array will have a 'href', 'etag' and 'event'
  780. * part, where the 'href' is relative to the calendar and the event contains the
  781. * definition of the event in iCalendar format.
  782. *
  783. * @param timestamp $start The start time for the period
  784. * @param timestamp $finish The finish time for the period
  785. * @param boolean $completed Whether to include completed tasks
  786. * @param boolean $cancelled Whether to include cancelled tasks
  787. * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
  788. *
  789. * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
  790. */
  791. function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = "" ) {
  792. if ( $start && $finish ) {
  793. $time_range = <<<EOTIME
  794. <C:time-range start="$start" end="$finish"/>
  795. EOTIME;
  796. }
  797. // Warning! May contain traces of double negatives...
  798. $neg_cancelled = ( $cancelled === true ? "no" : "yes" );
  799. $neg_completed = ( $cancelled === true ? "no" : "yes" );
  800. $filter = <<<EOFILTER
  801. <C:filter>
  802. <C:comp-filter name="VCALENDAR">
  803. <C:comp-filter name="VTODO">
  804. <C:prop-filter name="STATUS">
  805. <C:text-match negate-condition="$neg_completed">COMPLETED</C:text-match>
  806. </C:prop-filter>
  807. <C:prop-filter name="STATUS">
  808. <C:text-match negate-condition="$neg_cancelled">CANCELLED</C:text-match>
  809. </C:prop-filter>$time_range
  810. </C:comp-filter>
  811. </C:comp-filter>
  812. </C:filter>
  813. EOFILTER;
  814. return $this->DoCalendarQuery($filter, $relative_url);
  815. }
  816. /**
  817. * Get the calendar entry by UID
  818. *
  819. * @param uid
  820. * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
  821. * @param string $component_type The component type inside the VCALENDAR. Default 'VEVENT'.
  822. *
  823. * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery()
  824. */
  825. function GetEntryByUid( $uid, $relative_url = '', $component_type = 'VEVENT' ) {
  826. $filter = "";
  827. if ( $uid ) {
  828. $filter = <<<EOFILTER
  829. <C:filter>
  830. <C:comp-filter name="VCALENDAR">
  831. <C:comp-filter name="$component_type">
  832. <C:prop-filter name="UID">
  833. <C:text-match icollation="i;octet">$uid</C:text-match>
  834. </C:prop-filter>
  835. </C:comp-filter>
  836. </C:comp-filter>
  837. </C:filter>
  838. EOFILTER;
  839. }
  840. return $this->DoCalendarQuery($filter, $relative_url);
  841. }
  842. /**
  843. * Get the calendar entry by HREF
  844. *
  845. * @param string $href The href from a call to GetEvents or GetTodos etc.
  846. *
  847. * @return string The iCalendar of the calendar entry
  848. */
  849. function GetEntryByHref( $href ) {
  850. $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
  851. return $this->DoGETRequest( $href );
  852. }
  853. }
  854. /**
  855. * Usage example
  856. *
  857. * $cal = new CalDAVClient( "http://calendar.example.com/caldav.php/username/calendar/", "username", "password", "calendar" );
  858. * $options = $cal->DoOptionsRequest();
  859. * if ( isset($options["PROPFIND"]) ) {
  860. * // Fetch some information about the events in that calendar
  861. * $cal->SetDepth(1);
  862. * $folder_xml = $cal->DoXMLRequest("PROPFIND", '<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><getcontentlength/><getcontenttype/><resourcetype/><getetag/></prop></propfind>' );
  863. * }
  864. * // Fetch all events for February
  865. * $events = $cal->GetEvents("20070101T000000Z","20070201T000000Z");
  866. * foreach ( $events AS $k => $event ) {
  867. * do_something_with_event_data( $event['data'] );
  868. * }
  869. * $acc = array();
  870. * $acc["google"] = array(
  871. * "user"=>"kunsttherapie@gmail.com",
  872. * "pass"=>"xxxxx",
  873. * "server"=>"ssl://www.google.com",
  874. * "port"=>"443",
  875. * "uri"=>"https://www.google.com/calendar/dav/kunsttherapie@gmail.com/events/",
  876. * );
  877. *
  878. * $acc["davical"] = array(
  879. * "user"=>"some_user",
  880. * "pass"=>"big secret",
  881. * "server"=>"calendar.foo.bar",
  882. * "port"=>"80",
  883. * "uri"=>"http://calendar.foo.bar/caldav.php/some_user/home/",
  884. * );
  885. * //*******************************
  886. *
  887. * $account = $acc["davical"];
  888. *
  889. * //*******************************
  890. * $cal = new CalDAVClient( $account["uri"], $account["user"], $account["pass"], "", $account["server"], $account["port"] );
  891. * $options = $cal->DoOptionsRequest();
  892. * print_r($options);
  893. *
  894. * //*******************************
  895. * //*******************************
  896. *
  897. * $xmlC = <<<PROPP
  898. * <?xml version="1.0" encoding="utf-8" ?>
  899. * <D:propfind xmlns:D="DAV:" xmlns:C="http://calendarserver.org/ns/">
  900. * <D:prop>
  901. * <D:displayname />
  902. * <C:getctag />
  903. * <D:resourcetype />
  904. *
  905. * </D:prop>
  906. * </D:propfind>
  907. * PROPP;
  908. * //if ( isset($options["PROPFIND"]) ) {
  909. * // Fetch some information about the events in that calendar
  910. * // $cal->SetDepth(1);
  911. * // $folder_xml = $cal->DoXMLRequest("PROPFIND", $xmlC);
  912. * // print_r( $folder_xml);
  913. * //}
  914. *
  915. * // Fetch all events for February
  916. * $events = $cal->GetEvents("20090201T000000Z","20090301T000000Z");
  917. * foreach ( $events as $k => $event ) {
  918. * print_r($event['data']);
  919. * print "\n---------------------------------------------\n";
  920. * }
  921. *
  922. * //*******************************
  923. * //*******************************
  924. */