Overview

Packages

  • awl
    • caldav-client-v2
    • RRule
  • davical
    • authentication
      • drivers
    • caldav
    • DAViCalSession
    • DAVTicket
    • external-bind
    • feed
    • HTTPAuthSession
    • iSchedule
    • iSchedule-POST
    • logging
    • metrics
    • Principal
    • propfind
    • PublicSession
    • Request
    • Resource
    • tzservice
  • None
  • PHP

Classes

  • CalendarInfo
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  • Todo
   1: <?php
   2: /**
   3: * A Class for connecting to a caldav server
   4: *
   5: * @package   awl
   6: *
   7: * @subpackage caldav-client-v2
   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: 
  13: require_once('XMLDocument.php');
  14: 
  15: /**
  16:  * A class for holding basic calendar information
  17:  * @package awl
  18:  */
  19: class CalendarInfo {
  20:   public $url;
  21:   public $displayname;
  22:   public $getctag;
  23: 
  24:   function __construct( $url, $displayname = null, $getctag = null ) {
  25:     $this->url = $url;
  26:     $this->displayname = $displayname;
  27:     $this->getctag = $getctag;
  28:   }
  29: 
  30:   function __toString() {
  31:     return( '(URL: '.$this->url.'   Ctag: '.$this->getctag.'   Displayname: '.$this->displayname .')'. "\n" );
  32:   }
  33: }
  34: 
  35: if(!defined("_FSOCK_TIMEOUT")){
  36:   define("_FSOCK_TIMEOUT", 10);
  37: }
  38: 
  39: /**
  40: * A class for accessing DAViCal via CalDAV, as a client
  41: *
  42: * @package   awl
  43: */
  44: class CalDAVClient {
  45:   /**
  46:   * Server, username, password, calendar
  47:   *
  48:   * @var string
  49:   */
  50:   protected $base_url, $user, $pass, $entry, $protocol, $server, $port;
  51: 
  52:   /**
  53:   * The principal-URL we're using
  54:   */
  55:   protected $principal_url;
  56: 
  57:   /**
  58:   * The calendar-URL we're using
  59:   */
  60:   protected $calendar_url;
  61: 
  62:   /**
  63:   * The calendar-home-set we're using
  64:   */
  65:   protected $calendar_home_set;
  66: 
  67:   /**
  68:   * The calendar_urls we have discovered
  69:   */
  70:   protected $calendar_urls;
  71: 
  72:   /**
  73:   * The useragent which is send to the caldav server
  74:   *
  75:   * @var string
  76:   */
  77:   public $user_agent = 'DAViCalClient';
  78: 
  79:   protected $headers = array();
  80:   protected $body = "";
  81:   protected $requestMethod = "GET";
  82:   protected $httpRequest = "";  // for debugging http headers sent
  83:   protected $xmlRequest = "";   // for debugging xml sent
  84:   protected $xmlResponse = "";  // xml received
  85:   protected $httpResponseCode = 0; // http response code
  86:   protected $httpResponseHeaders = "";
  87:   protected $httpParsedHeaders;
  88:   protected $httpResponseBody = "";
  89: 
  90:   protected $parser; // our XML parser object
  91: 
  92:   private $debug = false; // Whether we are debugging
  93: 
  94:   /**
  95:   * Constructor, initialises the class
  96:   *
  97:   * @param string $base_url  The URL for the calendar server
  98:   * @param string $user      The name of the user logging in
  99:   * @param string $pass      The password for that user
 100:   */
 101:   function __construct( $base_url, $user, $pass ) {
 102:     $this->user = $user;
 103:     $this->pass = $pass;
 104:     $this->headers = array();
 105: 
 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';
 111:         $this->port = 443;
 112:       }
 113:       else {
 114:         $this->protocol = 'tcp';
 115:         $this->port = 80;
 116:       }
 117:       if ( $matches[4] != '' ) {
 118:         $this->port = intval($matches[4]);
 119:       }
 120:     }
 121:     else {
 122:       trigger_error("Invalid URL: '".$base_url."'", E_USER_ERROR);
 123:     }
 124:   }
 125: 
 126: 
 127:   /**
 128:    * Call this to enable / disable debugging.  It will return the prior value of the debugging flag.
 129:    * @param boolean $new_value The new value for debugging.
 130:    * @return boolean The previous value, in case you want to restore it later.
 131:    */
 132:   function SetDebug( $new_value ) {
 133:     $old_value = $this->debug;
 134:     if ( $new_value )
 135:       $this->debug = true;
 136:     else
 137:       $this->debug = false;
 138:     return $old_value;
 139:   }
 140: 
 141: 
 142: 
 143:   /**
 144:   * Adds an If-Match or If-None-Match header
 145:   *
 146:   * @param bool $match to Match or Not to Match, that is the question!
 147:   * @param string $etag The etag to match / not match against.
 148:   */
 149:   function SetMatch( $match, $etag = '*' ) {
 150:     $this->headers['match'] = sprintf( "%s-Match: \"%s\"", ($match ? "If" : "If-None"), trim($etag,'"'));
 151:   }
 152: 
 153:   /**
 154:   * Add a Depth: header.  Valid values are 0, 1 or infinity
 155:   *
 156:   * @param int $depth  The depth, default to infinity
 157:   */
 158:   function SetDepth( $depth = '0' ) {
 159:     $this->headers['depth'] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") );
 160:   }
 161: 
 162:   /**
 163:   * Add a Depth: header.  Valid values are 1 or infinity
 164:   *
 165:   * @param int $depth  The depth, default to infinity
 166:   */
 167:   function SetUserAgent( $user_agent = null ) {
 168:     if ( !isset($user_agent) ) $user_agent = $this->user_agent;
 169:     $this->user_agent = $user_agent;
 170:   }
 171: 
 172:   /**
 173:   * Add a Content-type: header.
 174:   *
 175:   * @param string $type  The content type
 176:   */
 177:   function SetContentType( $type ) {
 178:     $this->headers['content-type'] = "Content-type: $type";
 179:   }
 180: 
 181:   /**
 182:   * Set the calendar_url we will be using for a while.
 183:   *
 184:   * @param string $url The calendar_url
 185:   */
 186:   function SetCalendar( $url ) {
 187:     $this->calendar_url = $url;
 188:   }
 189: 
 190:   /**
 191:   * Split response into httpResponse and xmlResponse
 192:   *
 193:   * @param string Response from server
 194:    */
 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 );
 203: 
 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)) );
 206: //        debug_print_backtrace();
 207: //        echo "\nNodes array............................................................\n"; print_r( $this->xmlnodes );
 208: //        echo "\nTags array............................................................\n";  print_r( $this->xmltags );
 209:         printf( "\nXML Reponse:\n%s\n", $this->xmlResponse );
 210:       }
 211: 
 212:       xml_parser_free($parser);
 213:     }
 214:   }
 215: 
 216:   /**
 217:   * Split httpResponseHeaders into an array of headers
 218:   *
 219:   * @return array of arrays of header lines
 220:    */
 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);
 227:       $last_header = '';
 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();
 234:         }
 235:         $this->httpParsedHeaders[$header_name][] = $cur_headers;
 236:       }
 237:     }
 238:     return $this->httpParsedHeaders;
 239:   }
 240: 
 241:   /**
 242:    * Output http request headers
 243:    *
 244:    * @return HTTP headers
 245:    */
 246:   function GetHttpRequest() {
 247:       return $this->httpRequest;
 248:   }
 249:   /**
 250:    * Output http response headers
 251:    *
 252:    * @return HTTP headers
 253:    */
 254:   function GetResponseHeaders() {
 255:       return $this->httpResponseHeaders;
 256:   }
 257:   /**
 258:    * Output http response body
 259:    *
 260:    * @return HTTP body
 261:    */
 262:   function GetResponseBody() {
 263:       return $this->httpResponseBody;
 264:   }
 265:   /**
 266:    * Output xml request
 267:    *
 268:    * @return raw xml
 269:    */
 270:   function GetXmlRequest() {
 271:       return $this->xmlRequest;
 272:   }
 273:   /**
 274:    * Output xml response
 275:    *
 276:    * @return raw xml
 277:    */
 278:   function GetXmlResponse() {
 279:       return $this->xmlResponse;
 280:   }
 281: 
 282:   /**
 283:   * Send a request to the server
 284:   *
 285:   * @param string $url The URL to make the request to
 286:   *
 287:   * @return string The content of the response from the server
 288:   */
 289:   function DoRequest( $url = null ) {
 290:     $headers = array();
 291: 
 292:     if ( !isset($url) ) $url = $this->base_url;
 293:     $this->request_url = $url;
 294:     $url = preg_replace('{^https?://[^/]+}', '', $url);
 295:     // URLencode if it isn't already
 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);
 303:     }
 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;
 307: 
 308:     if ( !isset($this->headers['content-type']) ) $this->headers['content-type'] = "Content-type: text/plain";
 309:     foreach( $this->headers as $ii => $head ) {
 310:       $headers[] = $head;
 311:     }
 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;
 317: 
 318:     $this->xmlResponse = '';
 319: 
 320:     $fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT); //error handling?
 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; }
 323:     $response = "";
 324:     while( !feof($fip) ) { $response .= fgets($fip,8192); }
 325:     fclose($fip);
 326: 
 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]);
 331:     else
 332:       $this->httpResponseCode = 0;
 333: 
 334:     $this->headers = array();  // reset the headers array for our next request
 335:     $this->ParseResponse($this->httpResponseBody);
 336:     return $response;
 337:   }
 338: 
 339: 
 340:   /**
 341:   * Unchunk a chunked response
 342:   */
 343:   function Unchunk() {
 344:     $content = '';
 345:     $chunks = $this->httpResponseBody;
 346:     // printf( "\n================================\n%s\n================================\n", $chunks );
 347:     do {
 348:       $bytes = 0;
 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]);
 353:         // printf( "Chunk size 0x%s (%d)\n", $octets, $bytes );
 354:         if ( $bytes > 0 ) {
 355:           // printf( "---------------------------------\n%s\n---------------------------------\n", substr($chunks,$pos,$bytes) );
 356:           $content .= substr($chunks,$pos,$bytes);
 357:           $chunks = substr($chunks,$pos + $bytes + 2);
 358:           // printf( "+++++++++++++++++++++++++++++++++\n%s\n+++++++++++++++++++++++++++++++++\n", $chunks );
 359:         }
 360:       }
 361:       else {
 362:         $content .= $chunks;
 363:       }
 364:     }
 365:     while( $bytes > 0 );
 366:     $this->httpResponseBody = $content;
 367:     // printf( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n%s\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", $content );
 368:   }
 369: 
 370: 
 371:   /**
 372:   * Send an OPTIONS request to the server
 373:   *
 374:   * @param string $url The URL to make the request to
 375:   *
 376:   * @return array The allowed options
 377:   */
 378:   function DoOptionsRequest( $url = null ) {
 379:     $this->requestMethod = "OPTIONS";
 380:     $this->body = "";
 381:     $this->DoRequest($url);
 382:     $this->ParseResponseHeaders();
 383:     $allowed = '';
 384:     foreach( $this->httpParsedHeaders['Allow'] as $allow_header ) {
 385:       $allowed .= preg_replace( '/^(Allow:)?\s+([a-z, ]+)\r?\n.*/is', '$1,', $allow_header );
 386:     }
 387:     $options = array_flip( preg_split( '/[, ]+/', trim($allowed, ', ') ));
 388:     return $options;
 389:   }
 390: 
 391: 
 392: 
 393:   /**
 394:   * Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR)
 395:   *
 396:   * @param string $method The method (PROPFIND, REPORT, etc) to use with the request
 397:   * @param string $xml The XML to send along with the request
 398:   * @param string $url The URL to make the request to
 399:   *
 400:   * @return array An array of the allowed methods
 401:   */
 402:   function DoXMLRequest( $request_method, $xml, $url = null ) {
 403:     $this->body = $xml;
 404:     $this->requestMethod = $request_method;
 405:     $this->SetContentType("text/xml");
 406:     return $this->DoRequest($url);
 407:   }
 408: 
 409: 
 410: 
 411:   /**
 412:   * Get a single item from the server.
 413:   *
 414:   * @param string $url The URL to GET
 415:   */
 416:   function DoGETRequest( $url ) {
 417:     $this->body = "";
 418:     $this->requestMethod = "GET";
 419:     return $this->DoRequest( $url );
 420:   }
 421: 
 422: 
 423:   /**
 424:   * Get the HEAD of a single item from the server.
 425:   *
 426:   * @param string $url The URL to HEAD
 427:   */
 428:   function DoHEADRequest( $url ) {
 429:     $this->body = "";
 430:     $this->requestMethod = "HEAD";
 431:     return $this->DoRequest( $url );
 432:   }
 433: 
 434: 
 435:   /**
 436:   * PUT a text/icalendar resource, returning the etag
 437:   *
 438:   * @param string $url The URL to make the request to
 439:   * @param string $icalendar The iCalendar resource to send to the server
 440:   * @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource.
 441:   *
 442:   * @return string The content of the response from the server
 443:   */
 444:   function DoPUTRequest( $url, $icalendar, $etag = null ) {
 445:     $this->body = $icalendar;
 446: 
 447:     $this->requestMethod = "PUT";
 448:     if ( $etag != null ) {
 449:       $this->SetMatch( ($etag != '*'), $etag );
 450:     }
 451:     $this->SetContentType('text/calendar; charset="utf-8"');
 452:     $this->DoRequest($url);
 453: 
 454:     $etag = null;
 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 );
 464:       }
 465:       $this->httpRequest = $save_request;
 466:       $this->httpResponseHeaders = $save_response_headers;
 467:     }
 468:     return $etag;
 469:   }
 470: 
 471: 
 472:   /**
 473:   * DELETE a text/icalendar resource
 474:   *
 475:   * @param string $url The URL to make the request to
 476:   * @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL.
 477:   *
 478:   * @return int The HTTP Result Code for the DELETE
 479:   */
 480:   function DoDELETERequest( $url, $etag = null ) {
 481:     $this->body = "";
 482: 
 483:     $this->requestMethod = "DELETE";
 484:     if ( $etag != null ) {
 485:       $this->SetMatch( true, $etag );
 486:     }
 487:     $this->DoRequest($url);
 488:     return $this->httpResponseCode;
 489:   }
 490: 
 491: 
 492:   /**
 493:   * Get a single item from the server.
 494:   *
 495:   * @param string $url The URL to PROPFIND on
 496:   */
 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);
 503:     }
 504: 
 505:     $this->body = $xml->Render('propfind',$prop );
 506: 
 507:     $this->requestMethod = "PROPFIND";
 508:     $this->SetContentType("text/xml");
 509:     $this->DoRequest($url);
 510:     return $this->GetXmlResponse();
 511:   }
 512: 
 513: 
 514:   /**
 515:   * Get/Set the Principal URL
 516:   *
 517:   * @param $url string The Principal URL to set
 518:   */
 519:   function PrincipalURL( $url = null ) {
 520:     if ( isset($url) ) {
 521:       $this->principal_url = $url;
 522:     }
 523:     return $this->principal_url;
 524:   }
 525: 
 526: 
 527:   /**
 528:   * Get/Set the calendar-home-set URL
 529:   *
 530:   * @param $url array of string The calendar-home-set URLs to set
 531:   */
 532:   function CalendarHomeSet( $urls = null ) {
 533:     if ( isset($urls) ) {
 534:       if ( ! is_array($urls) ) $urls = array($urls);
 535:       $this->calendar_home_set = $urls;
 536:     }
 537:     return $this->calendar_home_set;
 538:   }
 539: 
 540: 
 541:   /**
 542:   * Get/Set the calendar-home-set URL
 543:   *
 544:   * @param $urls array of string The calendar URLs to set
 545:   */
 546:   function CalendarUrls( $urls = null ) {
 547:     if ( isset($urls) ) {
 548:       if ( ! is_array($urls) ) $urls = array($urls);
 549:       $this->calendar_urls = $urls;
 550:     }
 551:     return $this->calendar_urls;
 552:   }
 553: 
 554: 
 555:   /**
 556:   * Return the first occurrence of an href inside the named tag.
 557:   *
 558:   * @param string $tagname The tag name to find the href inside of
 559:   */
 560:   function HrefValueInside( $tagname ) {
 561:     foreach( $this->xmltags[$tagname] AS $k => $v ) {
 562:       $j = $v + 1;
 563:       if ( $this->xmlnodes[$j]['tag'] == 'DAV::href' ) {
 564:         return rawurldecode($this->xmlnodes[$j]['value']);
 565:       }
 566:     }
 567:     return null;
 568:   }
 569: 
 570: 
 571:   /**
 572:   * Return the href containing this property.  Except only if it's inside a status != 200
 573:   *
 574:   * @param string $tagname The tag name of the property to find the href for
 575:   * @param integer $which Which instance of the tag should we use
 576:   */
 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' ) {
 581: //        printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
 582:         if ( $this->xmlnodes[$j]['tag'] == 'DAV::status' && $this->xmlnodes[$j]['value'] != 'HTTP/1.1 200 OK' ) return null;
 583:       }
 584: //      printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
 585:       if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
 586: //        printf( "Value[$j]: %s\n", $this->xmlnodes[$j]['value']);
 587:         return rawurldecode($this->xmlnodes[$j]['value']);
 588:       }
 589:     }
 590:     else {
 591:       if ( $this->debug ) printf( "xmltags[$tagname] or xmltags[$tagname][$i] is not set\n");
 592:     }
 593:     return null;
 594:   }
 595: 
 596: 
 597:   /**
 598:   * Return the href which has a resourcetype of the specified type
 599:   *
 600:   * @param string $tagname The tag name of the resourcetype to find the href for
 601:   * @param integer $which Which instance of the tag should we use
 602:   */
 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' );
 607:       if ( $j > 0 ) {
 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']);
 611:         }
 612:       }
 613:     }
 614:     return null;
 615:   }
 616: 
 617: 
 618:   /**
 619:   * Return the <prop> ... </prop> of a propstat where the status is OK
 620:   *
 621:   * @param string $nodenum The node number in the xmlnodes which is the href
 622:   */
 623:   function GetOKProps( $nodenum ) {
 624:     $props = null;
 625:     $level = $this->xmlnodes[$nodenum]['level'];
 626:     $status = '';
 627:     while ( $this->xmlnodes[++$nodenum]['level'] >= $level ) {
 628:       if ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::propstat' ) {
 629:         if ( $this->xmlnodes[$nodenum]['type'] == 'open' ) {
 630:           $props = array();
 631:           $status = '';
 632:         }
 633:         else {
 634:           if ( $status == 'HTTP/1.1 200 OK' ) break;
 635:         }
 636:       }
 637:       elseif ( !isset($this->xmlnodes[$nodenum]) || !is_array($this->xmlnodes[$nodenum]) ) {
 638:         break;
 639:       }
 640:       elseif ( $this->xmlnodes[$nodenum]['tag'] == 'DAV::status' ) {
 641:         $status = $this->xmlnodes[$nodenum]['value'];
 642:       }
 643:       else {
 644:         $props[] = $this->xmlnodes[$nodenum];
 645:       }
 646:     }
 647:     return $props;
 648:   }
 649: 
 650: 
 651:   /**
 652:   * Attack the given URL in an attempt to find a principal URL
 653:   *
 654:   * @param string $url The URL to find the principal-URL from
 655:   */
 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);
 659: 
 660:     $principal_url = $this->HrefForProp('DAV::principal');
 661: 
 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);
 666:         }
 667:       }
 668:     }
 669: 
 670:     return $this->PrincipalURL($principal_url);
 671:   }
 672: 
 673: 
 674:   /**
 675:   * Attack the given URL in an attempt to find a principal URL
 676:   *
 677:   * @param string $url The URL to find the calendar-home-set from
 678:   */
 679:   function FindCalendarHome( $recursed=false ) {
 680:     if ( !isset($this->principal_url) ) {
 681:       $this->FindPrincipal();
 682:     }
 683:     if ( $recursed ) {
 684:       $this->DoPROPFINDRequest( $this->principal_url, array('urn:ietf:params:xml:ns:caldav:calendar-home-set'), 0);
 685:     }
 686: 
 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' ) {
 691: //        printf( "Tag: '%s' = '%s'\n", $this->xmlnodes[$v]['tag'], $this->xmlnodes[$v]['value']);
 692:         if ( $this->xmlnodes[$v]['tag'] == 'DAV::href' && isset($this->xmlnodes[$v]['value']) )
 693:           $calendar_home[] = rawurldecode($this->xmlnodes[$v]['value']);
 694:       }
 695:     }
 696: 
 697:     if ( !$recursed && count($calendar_home) < 1 ) {
 698:       $calendar_home = $this->FindCalendarHome(true);
 699:     }
 700: 
 701:     return $this->CalendarHomeSet($calendar_home);
 702:   }
 703: 
 704: 
 705:   /**
 706:   * Find the calendars, from the calendar_home_set
 707:   */
 708:   function FindCalendars( $recursed=false ) {
 709:     if ( !isset($this->calendar_home_set[0]) ) {
 710:       $this->FindCalendarHome();
 711:     }
 712:     $this->DoPROPFINDRequest( $this->calendar_home_set[0], array('resourcetype','displayname','http://calendarserver.org/ns/:getctag'), 1);
 713: 
 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;
 719:       }
 720: 
 721:       foreach( $this->xmltags['DAV::href'] AS $i => $hnode ) {
 722:         $href = rawurldecode($this->xmlnodes[$hnode]['value']);
 723: 
 724:         if ( !isset($calendar_urls[$href]) ) continue;
 725: 
 726: //        printf("Seems '%s' is a calendar.\n", $href );
 727: 
 728:         $calendar = new CalendarInfo($href);
 729:         $ok_props = $this->GetOKProps($hnode);
 730:         foreach( $ok_props AS $v ) {
 731: //          printf("Looking at: %s[%s]\n", $href, $v['tag'] );
 732:           switch( $v['tag'] ) {
 733:             case 'http://calendarserver.org/ns/:getctag':
 734:               $calendar->getctag = $v['value'];
 735:               break;
 736:             case 'DAV::displayname':
 737:               $calendar->displayname = $v['value'];
 738:               break;
 739:           }
 740:         }
 741:         $calendars[] = $calendar;
 742:       }
 743:     }
 744: 
 745:     return $this->CalendarUrls($calendars);
 746:   }
 747: 
 748: 
 749:   /**
 750:   * Find the calendars, from the calendar_home_set
 751:   */
 752:   function GetCalendarDetails( $url = null ) {
 753:     if ( isset($url) ) $this->SetCalendar($url);
 754: 
 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);
 757: 
 758:     $hnode = $this->xmltags['DAV::href'][0];
 759:     $href = rawurldecode($this->xmlnodes[$hnode]['value']);
 760: 
 761:     $calendar = new CalendarInfo($href);
 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'];
 767:       }
 768: /*      else {
 769:         printf( "Calendar property '%s' has no text content\n", $v['tag'] );
 770:       }*/
 771:     }
 772: 
 773:     return $calendar;
 774:   }
 775: 
 776: 
 777:   /**
 778:   * Get all etags for a calendar
 779:   */
 780:   function GetCollectionETags( $url = null ) {
 781:     if ( isset($url) ) $this->SetCalendar($url);
 782: 
 783:     $this->DoPROPFINDRequest( $this->calendar_url, array('getetag'), 1);
 784: 
 785:     $etags = array();
 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'];
 790:       }
 791:     }
 792: 
 793:     return $etags;
 794:   }
 795: 
 796: 
 797:   /**
 798:   * Get a bunch of events for a calendar with a calendar-multiget report
 799:   */
 800:   function CalendarMultiget( $event_hrefs, $url = null ) {
 801: 
 802:     if ( isset($url) ) $this->SetCalendar($url);
 803: 
 804:     $hrefs = '';
 805:     foreach( $event_hrefs AS $k => $href ) {
 806:       $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
 807:       $hrefs .= '<href>'.$href.'</href>';
 808:     }
 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>
 813: $hrefs
 814: </C:calendar-multiget>
 815: EOXML;
 816: 
 817:     $this->requestMethod = "REPORT";
 818:     $this->SetContentType("text/xml");
 819:     $this->DoRequest( $this->calendar_url );
 820: 
 821:     $events = array();
 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);
 825: //        echo "Calendar-data:\n"; print_r($this->xmlnodes[$v]);
 826:         $events[$href] = $this->xmlnodes[$v]['value'];
 827:       }
 828:     }
 829:     else {
 830:       foreach( $event_hrefs AS $k => $href ) {
 831:         $this->DoGETRequest($href);
 832:         $events[$href] = $this->httpResponseBody;
 833:       }
 834:     }
 835: 
 836:     return $events;
 837:   }
 838: 
 839: 
 840:   /**
 841:   * Given XML for a calendar query, return an array of the events (/todos) in the
 842:   * response.  Each event in the array will have a 'href', 'etag' and '$response_type'
 843:   * part, where the 'href' is relative to the calendar and the '$response_type' contains the
 844:   * definition of the calendar data in iCalendar format.
 845:   *
 846:   * @param string $filter XML fragment which is the <filter> element of a calendar-query
 847:   * @param string $url The URL of the calendar, or empty/null to use the 'current' calendar_url
 848:   *
 849:   * @return array An array of the relative URLs, etags, and events from the server.  Each element of the array will
 850:   *               be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied
 851:   *               etag (which only varies when the data changes) and the calendar data in iCalendar format.
 852:   */
 853:   function DoCalendarQuery( $filter, $url = '' ) {
 854: 
 855:     if ( !empty($url) ) $this->SetCalendar($url);
 856: 
 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">
 860:   <D:prop>
 861:     <C:calendar-data/>
 862:     <D:getetag/>
 863:   </D:prop>$filter
 864: </C:calendar-query>
 865: EOXML;
 866: 
 867:     $this->requestMethod = "REPORT";
 868:     $this->SetContentType("text/xml");
 869:     $this->DoRequest( $this->calendar_url );
 870: 
 871:     $report = array();
 872:     foreach( $this->xmlnodes as $k => $v ) {
 873:       switch( $v['tag'] ) {
 874:         case 'DAV::response':
 875:           if ( $v['type'] == 'open' ) {
 876:             $response = array();
 877:           }
 878:           elseif ( $v['type'] == 'close' ) {
 879:             $report[] = $response;
 880:           }
 881:           break;
 882:         case 'DAV::href':
 883:           $response['href'] = basename( rawurldecode($v['value']) );
 884:           break;
 885:         case 'DAV::getetag':
 886:           $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
 887:           break;
 888:         case 'urn:ietf:params:xml:ns:caldav:calendar-data':
 889:           $response['data'] = $v['value'];
 890:           break;
 891:       }
 892:     }
 893:     return $report;
 894:   }
 895: 
 896: 
 897:   /**
 898:   * Get the events in a range from $start to $finish.  The dates should be in the
 899:   * format yyyymmddThhmmssZ and should be in GMT.  The events are returned as an
 900:   * array of event arrays.  Each event array will have a 'href', 'etag' and 'event'
 901:   * part, where the 'href' is relative to the calendar and the event contains the
 902:   * definition of the event in iCalendar format.
 903:   *
 904:   * @param timestamp $start The start time for the period
 905:   * @param timestamp $finish The finish time for the period
 906:   * @param string    $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
 907:   *
 908:   * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
 909:   */
 910:   function GetEvents( $start = null, $finish = null, $relative_url = '' ) {
 911:     $filter = "";
 912:     if ( isset($start) && isset($finish) )
 913:         $range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
 914:     else
 915:         $range = '';
 916: 
 917:     $filter = <<<EOFILTER
 918:   <C:filter>
 919:     <C:comp-filter name="VCALENDAR">
 920:       <C:comp-filter name="VEVENT">
 921:         $range
 922:       </C:comp-filter>
 923:     </C:comp-filter>
 924:   </C:filter>
 925: EOFILTER;
 926: 
 927:     return $this->DoCalendarQuery($filter, $relative_url);
 928:   }
 929: 
 930: 
 931:   /**
 932:   * Get the todo's in a range from $start to $finish.  The dates should be in the
 933:   * format yyyymmddThhmmssZ and should be in GMT.  The events are returned as an
 934:   * array of event arrays.  Each event array will have a 'href', 'etag' and 'event'
 935:   * part, where the 'href' is relative to the calendar and the event contains the
 936:   * definition of the event in iCalendar format.
 937:   *
 938:   * @param timestamp $start The start time for the period
 939:   * @param timestamp $finish The finish time for the period
 940:   * @param boolean   $completed Whether to include completed tasks
 941:   * @param boolean   $cancelled Whether to include cancelled tasks
 942:   * @param string    $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
 943:   *
 944:   * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
 945:   */
 946:   function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = "" ) {
 947: 
 948:     if ( $start && $finish ) {
 949: $time_range = <<<EOTIME
 950:                 <C:time-range start="$start" end="$finish"/>
 951: EOTIME;
 952:     }
 953: 
 954:     // Warning!  May contain traces of double negatives...
 955:     $neg_cancelled = ( $cancelled === true ? "no" : "yes" );
 956:     $neg_completed = ( $cancelled === true ? "no" : "yes" );
 957: 
 958:     $filter = <<<EOFILTER
 959:   <C:filter>
 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>
 964:                 </C:prop-filter>
 965:                 <C:prop-filter name="STATUS">
 966:                         <C:text-match negate-condition="$neg_cancelled">CANCELLED</C:text-match>
 967:                 </C:prop-filter>$time_range
 968:           </C:comp-filter>
 969:     </C:comp-filter>
 970:   </C:filter>
 971: EOFILTER;
 972: 
 973:     return $this->DoCalendarQuery($filter, $relative_url);
 974:   }
 975: 
 976: 
 977:   /**
 978:   * Get the calendar entry by UID
 979:   *
 980:   * @param uid
 981:   * @param string    $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
 982:   * @param string    $component_type The component type inside the VCALENDAR.  Default 'VEVENT'.
 983:   *
 984:   * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery()
 985:   */
 986:   function GetEntryByUid( $uid, $relative_url = '', $component_type = 'VEVENT' ) {
 987:     $filter = "";
 988:     if ( $uid ) {
 989:       $filter = <<<EOFILTER
 990:   <C:filter>
 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>
 995:                 </C:prop-filter>
 996:           </C:comp-filter>
 997:     </C:comp-filter>
 998:   </C:filter>
 999: EOFILTER;
1000:     }
1001: 
1002:     return $this->DoCalendarQuery($filter, $relative_url);
1003:   }
1004: 
1005: 
1006:   /**
1007:   * Get the calendar entry by HREF
1008:   *
1009:   * @param string    $href         The href from a call to GetEvents or GetTodos etc.
1010:   *
1011:   * @return string The iCalendar of the calendar entry
1012:   */
1013:   function GetEntryByHref( $href ) {
1014:     $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
1015:     return $this->DoGETRequest( $href );
1016:   }
1017: 
1018: }
1019: 
DAViCal API documentation generated by ApiGen 2.8.0