<?php
/**
* A Class for connecting to a caldav server
*
* @package   awl
* removed curl - now using fsockopen
* changed 2009 by Andres Obrero - Switzerland andres@obrero.ch
*
* @subpackage   caldav
* @author Andrew McMillan <debian@mcmillan.net.nz>
* @copyright Andrew McMillan
* @license   http://gnu.org/copyleft/gpl.html GNU GPL v2
*
* MODIFIED : Atmail
*/

if(!defined('APPLE_DAV_SERVER'))		define('APPLE_DAV_SERVER', 'ap');
if(!defined('ATMAIL_DAV_SERVER'))		define('ATMAIL_DAV_SERVER', 'at');
if(!defined('UNKNOWN_DAV_SERVER'))		define('UNKNOWN_DAV_SERVER', 'un');
if(!defined('DAVICAL_DAV_SERVER'))		define('DAVICAL_DAV_SERVER', 'da');
if(!defined('BEDIWORK_DAV_SERVER'))		define('BEDIWORK_DAV_SERVER', 'be');
if(!defined('SABREDAV_DAV_SERVER'))		define('SABREDAV_DAV_SERVER', 'sd');
if(!defined('DAVICAL_DAV_SERVER_TEST'))		define('DAVICAL_DAV_SERVER_TEST', 'DAViCal');
if(!defined('BEDIWORK_DAV_SERVER_TEST'))	define('BEDIWORK_DAV_SERVER_TEST', 'Server: Apache-Coyote');
if(!defined('SABREDAV_DAV_SERVER_TEST'))	define('SABREDAV_DAV_SERVER_TEST', 'X-Sabre-Version');
if(!defined('TWISTED_DAV_SERVER_TEST'))		define('TWISTED_DAV_SERVER_TEST', 'TwistedWeb');
if(!defined('ATMAIL_DAV_SERVER_TEST'))		define('ATMAIL_DAV_SERVER_TEST', "Twisted/8.2.0+r26969");
if(!defined('DAV_UNAUTHORIZED'))		define('DAV_UNAUTHORIZED', "401 Unauthorized");
if(!defined('DAV_FORBIDDEN'))			define('DAV_FORBIDDEN', "403 Forbidden");

$digestInUse = false; // cached digest information

/**
* A class for accessing DAViCal via CalDAV, as a client
*
* @package   awl
*/
class CalDAVClient {
  /**
  * Server, username, password, calendar
  *
  * @var string
  */
  var $base_url, $user, $pass, $calendar, $entry, $protocol, $server, $port;
  var $last_nonce = '';
  var $last_digest = array();
  var $depth = 'infinity';

  /**
  * The useragent which is send to the caldav server
  *
  * @var string
  */
  var $user_agent = 'DAViCalClient';
  var $resultcode = 200;

  var $headers = array();
  var $body = "";
  var $requestMethod = "POST";
  var $httpRequest = ""; // for debugging http headers sent
  var $xmlRequest = ""; // for debugging xml sent
  var $httpResponse = ""; // for debugging http headers received
  var $xmlResponse = ""; // for debugging xml received
  var $type = 'un';
 
  /**
  * Constructor, initialises the class
  *
  * @param string $base_url  The URL for the calendar server
  * @param string $user      The name of the user logging in
  * @param string $pass      The password for that user
  * @param string $calendar  The name of the calendar (not currently used)
  */
  function CalDAVClient( $base_url, $user, $pass, $calendar ) {
    $this->user = $user;
    $this->pass = $pass;
    $this->calendar = $calendar;
    $this->headers = array();

    if ( preg_match( '#^(https?)://([a-z0-9.-]+)(:([0-9]+))?(/.*)$#', $base_url, $matches ) ) {
      $this->server = $matches[2];
      $this->base_url = $matches[5];

      if ( $matches[1] == 'https' ) {
        $this->protocol = 'ssl';
        $this->port = 443;
      }
      else {
        $this->protocol = 'tcp';
        $this->port = 80;
      }
      if ( $matches[4] != '' ) {
        $this->port = intval($matches[4]);
      }
    }
    else {
      throw new Exception("Invalid URL: '".$base_url."'");
    }
  }

  /**
  * Adds an If-Match or If-None-Match header
  *
  * @param bool $match to Match or Not to Match, that is the question!
  * @param string $etag The etag to match / not match against.
  */
  function SetMatch( $match, $etag = '*' ) {
    $this->headers[] = sprintf( "%s-Match: %s", ($match ? "If" : "If-None"), $etag);
  }

  /**
  * Add a Depth: header.  Valid values are 1 or infinity
  *
  * @param int $depth  The depth, default to infinity
  */
  function SetDepth( $depth = 'infinity' ) { 	
 	$this->depth = $depth;
  }
  
  function GetDepth()
  {
	return "Depth: ". ($this->depth == 1 ? "1" : ( $this->depth == 'zero' ? "0" : "infinity") );
  }

  /**
  * Add a Depth: header.  Valid values are 1 or infinity
  *
  * @param int $depth  The depth, default to infinity
  */
  function SetUserAgent( $user_agent = null ) {
    if ( !isset($user_agent) ) $user_agent = $this->user_agent;
    $this->user_agent = $user_agent;
  }

  /**
  * Add a Content-type: header.
  *
  * @param int $type  The content type
  */
  function SetContentType( $type ) {
  	if( is_array($this->headers) )
  	{
	  	$old_type = array_search( $type, $this->headers );
  		if($old_type != FALSE)
  		{
  			// we found an old content-type header directive
  			// nuke it
  			unset($this->headers[$old_type]);
  			$this->headers = (is_array($this->headers) ? array_values($this->headers) : array());	
  		}
  	}
  	$this->headers[] = "Content-type: $type";
  }

  /**
  * Split response into httpResponse and xmlResponse
  *
  * @param string Response from server
   */
  function ParseResponse( $response ) {
      $pos = strpos($response, '<?xml');
      if ($pos == false) {
        $this->httpResponse = trim($response);
      }
      else {
        $this->httpResponse = trim(substr($response, 0, $pos));
        $this->xmlResponse = trim(substr($response, $pos));
      }
  }

  /**
   * Output http request headers
   *
   * @return HTTP headers
   */
  function GetHttpRequest() {
      return $this->httpRequest;
  }
  /**
   * Output http response headers
   *
   * @return HTTP headers
   */
  function GetHttpResponse() {
      return $this->httpResponse;
  }
  /**
   * Output xml request
   *
   * @return raw xml
   */
  function GetXmlRequest() {
      return $this->xmlRequest;
  }
  /**
   * Output xml response
   *
   * @return raw xml
   */
  function GetXmlResponse() {
      return $this->xmlResponse;
  }
  
function extract_digest_information($type, $rsp)
{
	$result = '';
	
  	if(strstr($rsp, $type . '=') != NULL)
	{
		list($junk, $result) = explode($type . '=', $rsp);
		if(strstr($result, ',') != NULL)
		{
			list($result, $junk) = explode(',', $result);
		}
		else
		{	
			list($result, $junk) = explode("\n", $result);	
		}
		$result = trim($result, "\r");
		$result = trim($result, '"');
	}
	
	return $result;
}

function getDataDirectory()
{
	$temp_user = str_replace("@", "_", $this->user);
	$components = str_split($temp_user, 2);
	list($user, $domain) = explode("@", $this->user, 2);
	$url = "/usr/local/atmail/calendarserver/server/twistedcaldav/atmail/data/calendars/__uids__/" . $components[0] . "/" . $components[1] . "/" . $user . "_" . $domain . "/calendar/";
	$test = is_file($url . ".db.sqlite");
	if($test == false)
	{
		return false;
	}
	return $url;
}

/**
* Send a request to the server
*
* @param string $relative_url The URL to make the request to, relative to $base_url
*
* @return string The content of the response from the server
*/
function DoRequest( $relative_url = "", $digest = array(), $retries = 3, $input_headers = array() )
{
	global $digestInUse;
	if($relative_url == '')
		$relative_url = "/";
		
	if($this->base_url != '' && $this->base_url[strlen($this->base_url) - 1] == '/' && $relative_url == '/')
		$this->base_url = preg_replace('/\/$/', '', $this->base_url);

	if($retries == 0)
	{
		return null;
	}

	$retries --;

	if(!defined("_FSOCK_TIMEOUT"))
	{
		define("_FSOCK_TIMEOUT", 10);
	}

	$headers = array();
			
	$headers[] = $this->requestMethod . " " . $this->base_url . $relative_url . " HTTP/1.1";

	if( $digestInUse != false && ((isset($digestInUse['algorithm']) && $digestInUse['algorithm'] == 'basic') || $digestInUse['nonce'] != '') )
	{
		$digest = $digestInUse;
	}

	if( isset($digest['algorithm']) && $digest['algorithm'] == 'basic' )
	{
		$headers[] = "Authorization: Basic ".base64_encode($this->user .":". $this->pass );
	}
	else if ( isset($digest['algorithm']) && $digest['algorithm'] == 'md5' )
	{
		// Both RFC 2069 && RFC 2617
		// 	HA1 = MD5( user:realm:pass )
		// if qop is undefined or == 'auth'
		// 	HA2 = MD5( requestMethod:URI )
		// else if qop is 'auth-int' - we dont support this.
		// 	HA2 = MD5 ( requestMethod:URI:MD5(entityBody) )
		// if qop is undefined
		// 	RESPONSE = MD5( HA1:nonce:HA2 )
		// else if qop is 'auth' or 'auth-int'
		// 	RESPONSE = MD5( HA1:nonce:nc:cnonce:qop:HA2 )

		if(isset($digest['qop']) && $digest['qop'] == "auth-int")
		{
			// auth-int unsupported.
			return NULL;
		}
		
		// HA1 = MD5( user:realm:pass )
		$ha1 = md5($this->user . ':' . $digest['realm'] . ':' . $this->pass);

		// HA2 calcaulated the same if even optional quality of protection is not in use.
		// HA2 = MD5( requestMethod:URI )
		if($this->base_url == '' && $relative_url == '' && $digest['qop'] != '')
		{
			$ha2 = md5($this->requestMethod . ':""');
		}
		else
		{
			$ha2 = md5($this->requestMethod . ':' . $this->base_url . $relative_url);			
		}
		
		if( isset($digest['qop']) && $digest['qop'] == '')
		{
			// qop is undefined
			// RESPONSE = MD5( HA1:nonce:HA2 )
			$response = md5( $ha1 . ':' . $digest['nonce'] . ':' . $ha2);
			$digest_reponse = 'Authorization: Digest username="' . $this->user . '", realm="' . $digest['realm'] . '", nonce="' . $digest['nonce'] . '", uri="' . $this->base_url . $relative_url . '", response="' . $response . '", algorithm="' . $digest['algorithm'] . '"';
			if(isset($digest['opaque']) && $digest['opaque'] != '')
			{
				$digest_reponse .= ', opaque="' . $digest['opaque'] . '"';	
			}

		}
		else if ( isset($digest['qop']) && $digest['qop'] == 'auth')
		{
			// qop is in use	
			/*
			nonce-count
			     This MUST be specified if a qop directive is sent (see above), and
			     MUST NOT be specified if the server did not send a qop directive in
			     the WWW-Authenticate header field.  The nc-value is the hexadecimal
			     count of the number of requests (including the current request)
			     that the client has sent with the nonce value in this request.  For
			     example, in the first request sent in response to a given nonce
			     value, the client sends "nc=00000001".  The purpose of this
			     directive is to allow the server to detect request replays by
			     maintaining its own copy of this count - if the same nc-value is
			     seen twice, then the request is a replay.   See the description
			     below of the construction of the request-digest value.
			*/
			if(!isset($digest['nc']))
				$digest['nc'] = 0;
			$digest['nc'] ++;
			
			/*
				cnonce
				
				a client created nonce value
				cnonce="0a4f113b"
			*/
			if(!isset($digest['cnonce']))
				$digest['cnonce'] = uniqid();
			
			// RESPONSE = MD5( HA1:nonce:nc:cnonce:qop:HA2 )
			$toDigest = sprintf("%s:%s:%08d:%s:%s:%s", $ha1, $digest['nonce'], $digest['nc'], $digest['cnonce'], $digest['qop'], $ha2);
			
			$response = md5( $toDigest );
			/*
			  Authorization: Digest username="Mufasa",
	                 realm="testrealm@host.com",
	                 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
	                 uri="/dir/index.html",
	                 qop=auth,
	                 nc=00000001,
	                 cnonce="0a4f113b",
	                 response="6629fae49393a05397450978507c4ef1",
	                 opaque="5ccc069c403ebaf9f0171e9517f40e41"
			*/
			$digest_reponse = sprintf('Authorization: Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%08d, cnonce="%s", response="%s", opaque="%s"', 
						$this->user, 
						$digest['realm'],
						$digest['nonce'],
						$this->base_url . $relative_url,
						$digest['qop'],
						$digest['nc'],
						$digest['cnonce'],
						$response,
						$digest['opaque']);
		}
		else
		{
			$digestInUse = false;
			// unsupported or we have lost our mind - abort!
			return NULL;	
		}
		$headers[] = $digest_reponse;
	}

	$headers[] = "Host: ".$this->server .":".$this->port;

	// header caching
	if(!count($this->headers) && count($input_headers))
	{
		$this->headers = $input_headers;
	}
	$original_headers = $this->headers;

	// ensure content type has been set.
	$content_type_set = false;
	foreach( $this->headers as $ii => $head ) {
		$headers[] = $head;
		if(strstr($head, "Content-type") != NULL)
		{
			$content_type_set = true;
		}
	}
	if(!$content_type_set)
	{
		$headers[] = "Content-type: text/xml";
	}

	$headers[] = "Content-Length: " . strlen($this->body);
	$headers[] = "User-Agent: " . $this->user_agent;
	$headers[] = "Pragma: no-cache";
	$headers[] = "Cache-Control: no-cache";
	$headers[] = $this->GetDepth();
	$headers[] = "Accept: */*";
//	$headers[] = "Accept: text/xml";
	$headers[] = 'Connection: close';

	$this->httpRequest = join("\r\n",$headers);
	$this->xmlRequest = $this->body;

	$err_level = error_reporting(E_ERROR | E_COMPILE_ERROR);
	try
	{
		if($this->protocol == 'ssl')
		{
			$protocols = array('ssl', 'sslv2', 'sslv3', 'tls');
			foreach($protocols as $protocol)
			{
				$fip = fsockopen( $protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT);
				if($fip != FALSE)
					break;
			}				
			if($fip != FALSE)
			{
				$this->procotol = $protocol;
			}
		}
		else
		{
			$fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT);
		}

		if($fip)
		{
			if ( !(get_resource_type($fip) == 'stream') ) 
			{
				return false;
			}
			if ( !fwrite($fip, $this->httpRequest."\r\n\r\n".$this->body) )
			{
				fclose($fip);
				return false;
			}
			$rsp = "";
			while( !feof($fip) ) 
			{
				$rsp .= fgets($fip,8192);
			}
			fclose($fip);

			$this->headers = array();  // reset the headers array for our next request
			$this->ParseResponse($rsp);
			$this->last_digest = $digest;
		}
		else
		{
			if($errno == 111)
			{
				// connection refused, abort
				return false;
			}
			$rsp = "";
		}
	}
	catch (Exception $e)
	{
		$rsp = "";
	}

	error_reporting($err_level);

	$teststr = strtolower($rsp);
	if((strstr($teststr, "www-authenticate: negotiate") != null || strstr($teststr, "www-authenticate: digest")) != null && (!isset($digest['nonce']) || (isset($digest['nonce']) && $digest['nonce'] == "")))
	{
		// digest authentication requested.
		$digest_info = array();
		$digest_info['realm'] = $this->extract_digest_information('realm', $rsp);
		$digest_info['qop'] = $this->extract_digest_information('qop', $rsp);
		$digest_info['domain'] = $this->extract_digest_information('domain', $rsp);

		$digest_info['stale'] = $this->extract_digest_information('stale', $rsp); // as we dont keep alive, we can probably ignore stale
		$digest_info['nonce'] = $this->extract_digest_information('nonce', $rsp);
		$digest_info['algorithm'] = $this->extract_digest_information('algorithm', $rsp);
		$digest_info['opaque'] = $this->extract_digest_information('opaque', $rsp);

		if($digest_info['algorithm'] == '')
			$digest_info['algorithm'] = 'md5';

		$digest_info['last_nonce'] = '';

		return $this->DoRequest( $relative_url, $digest_info, $retries, $original_headers );
	}
	else
	{
		if(!isset($digest['nonce']))
		{
			$digest['nonce'] = '';
		}
		$digest['last_nonce'] = $digest['nonce'];
	}


	if(strstr($rsp, "HTTP/1.1 401 Unauthorized") != null)
	{
		$digestInUse = false;

		// Authorization failure
		if(!isset($digest_info['algorithm']) || (isset($digest_info['algorithm']) && $digest_info['algorithm'] == ''))
		{
			// try basic
			$digest_info['nonce'] = '';
			$digest_info['realm'] = '';
			$digest_info['algorithm'] = 'basic';
			return $this->DoRequest( $relative_url, $digest_info, $retries, $original_headers );
		}
	}
	else
	{
		$digestInUse = $digest;
	}

	$this->depth = 'infinity';
	
	return $rsp;
}

/**
* Send an OPTIONS request to the server
* But examine the results to judge the server type
* @param string $relative_url The URL to make the request to, relative to $base_url
* @return string The server type
*/
function DoServerCheckType( $relative_url = "" ) {
	$this->requestMethod = "OPTIONS";
	$this->body = "";

	$headers = $this->DoRequest($relative_url);

	if( $headers == NULL || $headers == "" || strstr($headers, DAV_UNAUTHORIZED) != null)
	{
		return false;
	}
	$type = UNKNOWN_DAV_SERVER;	
	
	if(strstr($headers, DAVICAL_DAV_SERVER_TEST) != null )
	{
		$type = DAVICAL_DAV_SERVER;
	}
	else if(strstr($headers, BEDIWORK_DAV_SERVER_TEST) != null )
	{
		$type = BEDIWORK_DAV_SERVER;
	}
	else if(strstr($headers, SABREDAV_DAV_SERVER_TEST) != null )
	{
		$type = SABREDAV_DAV_SERVER;
	}
	else if ( strstr($headers, TWISTED_DAV_SERVER_TEST) != null )
	{
		if( strstr($headers, ATMAIL_DAV_SERVER_TEST) != null)
		{
			$type = ATMAIL_DAV_SERVER;
		}
		else
		{
			$type = APPLE_DAV_SERVER;
		}
	}

	if( $type == BEDIWORK_DAV_SERVER )
	{
		if( strstr($headers, DAV_UNAUTHORIZED) != null)
		{
			return false;
		}
	}
	else
	{
		if( $headers == NULL || $headers == '' || strstr($headers, DAV_FORBIDDEN) != null)
		{
			return false;
		}
	}

	$this->type = $type;
	return $type;
}

/**
* Send an OPTIONS request to the server
*
* @param string $relative_url The URL to make the request to, relative to $base_url
*
* @return array The allowed options
*/
function DoOptionsRequest( $relative_url = "" ) {
	$this->requestMethod = "OPTIONS";
	$this->body = "";

	$headers = $this->DoRequest($relative_url);
	
	$options_header = preg_replace( '/^.*Allow: ([a-z, ]+)\r?\n.*/is', '$1', $headers );
	$options = array_flip( preg_split( '/[, ]+/', $options_header ));
	return $options;
}



  /**
  * Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR)
  *
  * @param string $method The method (PROPFIND, REPORT, etc) to use with the request
  * @param string $xml The XML to send along with the request
  * @param string $relative_url The URL to make the request to, relative to $base_url
  *
  * @return array An array of the allowed methods
  */
  function DoXMLRequest( $request_method, $xml, $relative_url = '' ) {
    $this->body = $xml;
    $this->requestMethod = $request_method;
    $this->SetContentType("text/xml");
    $result = $this->DoRequest($relative_url);
	return $result;
   
  }



  /**
  * Get a single item from the server.
  *
  * @param string $relative_url The part of the URL after the calendar
  */
  function DoGETRequest( $relative_url ) {
    $this->body = "";
    $this->requestMethod = "GET";
    return $this->DoRequest( $relative_url );
  }


  /**
  * PUT a text/icalendar resource, returning the etag
  *
  * @param string $relative_url The URL to make the request to, relative to $base_url
  * @param string $icalendar The iCalendar resource to send to the server
  * @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource.
  *
  * @return string The content of the response from the server
  */
  function DoPUTRequest( $relative_url, $icalendar, $etag = null, $contentType = "text/calendar" ) {
    $this->body = $icalendar;

    $this->requestMethod = "PUT";
    if ( $etag != null )
    {
      $this->SetMatch( ($etag != '*'), $etag );
    }
    // looks like the caldav server from apple DOESNT LIKE THE MIME text/icalendar!!!!
    $this->SetContentType($contentType);
    $headers = $this->DoRequest($relative_url);

	if(strpos($headers, 'error') != FALSE)
	{
		if(strpos($headers, 'no-uid-conflict') != FALSE)
		{
			$etag = 'no-uid-conflict';
		}
	}
	else
	{
	    /**
	    * RSCDS will always return the real etag on PUT.  Other CalDAV servers may need
	    * more work, but we are assuming we are running against RSCDS in this case.
	    */
	    $etag = preg_replace( '/^.*Etag: "?([^"\r\n]+)"?\r?\n.*/is', '$1', $headers );
	}
    return $etag;
  }


  /**
  * DELETE a text/icalendar resource
  *
  * @param string $relative_url The URL to make the request to, relative to $base_url
  * @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL.
  *
  * @return int The HTTP Result Code for the DELETE
  */
  function DoDELETERequest( $relative_url, $etag = null ) {
    $this->body = "";

    $this->requestMethod = "DELETE";
    if ( $etag != null ) {
      $this->SetMatch( true, $etag );
    }
    $this->DoRequest($relative_url);
  return $this->resultcode;
  }


  /**
  * Get the abook entry by UID
  *
  * @param uid
  * @param string    $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
  *
  * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery()
  */
  function GetVcardByUid( $uid, $relative_url = '' ) {
    $filter = "";
    
    if ( $uid ) {

$filter = <<<EOFILTER
<C:filter>
       <C:prop-filter name="UID">
         <C:text-match collation="i;unicode-casemap" match-type="equals">$uid</C:text-match>
       </C:prop-filter>
     </C:filter>
EOFILTER;

    }

    return $this->DoAddressbookQuery($filter, $relative_url);
  }

	function getXmlProperty($xml_tags, $properties, $pos=0)
	{
		$found['value'] = false;
		for($a = $pos; $a < count($xml_tags); $a ++)
		{
			if($xml_tags[$a]['tag'] == $properties[0])
			{
				$properties = array_slice($properties, 1);
				$found = $xml_tags[$a];
				break;
			}
		}
		if($found && count($properties) > 0)
			return $this->getXmlProperty($xml_tags, $properties, $a);
		else
			return $found['value'];
	}

	function GeneratePrincipalUrlFromConfig($current_principal, $url)
	{
		$url_array = parse_url($url);
		return $url_array['schema'] . '://' . $url_array['host'] . ":" . $url_array['port'] . '/' . $current_principal;
	}


	function GetCurrentPrincipal( )
	{
$xml = <<<EOXML
<?xml version="1.0" encoding="utf-8"?>
<D:propfind xmlns:D="DAV:" xmlns:C="DAV:">
<D:prop>
<C:current-user-principal/>
</D:prop>
</D:propfind>
EOXML;

		$this->SetDepth('zero');
		$this->DoXMLRequest( 'PROPFIND', $xml, '' );
		$xml_parser = xml_parser_create_ns('UTF-8');
		$this->xml_tags = array();
		$xml_parser_old = $xml_parser;
		try
		{
			xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
		}
		catch(Exception $e)
		{
			// restore old xml parser
			$xml_parser = $xml_parser_old;
		}
		xml_parse_into_struct( $xml_parser, $this->xmlResponse, $this->xml_tags );
		xml_parser_free($xml_parser);

		$response = $this->getXmlProperty($this->xml_tags, array('DAV::CURRENT-USER-PRINCIPAL', 'DAV::HREF'));

		$this->SetDepth();
		return $response;
	}
	
	function GetHomeFolder()
	{
$xml = <<<EOXML
<?xml version="1.0" encoding="utf-8"?>
<D:propfind xmlns:D="DAV:">
<D:prop>
<C:calendar-home-set xmlns:C="urn:ietf:params:xml:ns:caldav"/>
</D:prop>
</D:propfind>
EOXML;

		$this->SetDepth('zero');
		$this->DoXMLRequest( 'PROPFIND', $xml, '' );

		$xml_parser = xml_parser_create_ns('UTF-8');
		$this->xml_tags = array();
		xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
		xml_parse_into_struct( $xml_parser, $this->xmlResponse, $this->xml_tags );
		xml_parser_free($xml_parser);
		$response = $this->getXmlProperty($this->xml_tags, array('URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-HOME-SET', 'DAV::HREF'));

		$this->SetDepth();
		return $response;
	}

  function DoAddressbookQuery( $filter, $relative_url = '' ) {

    $xml = <<<EOXML
<?xml version="1.0" encoding="utf-8" ?>
<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
 <D:prop>
  <D:getetag/>
  <C:address-data>
   <C:allprop />
  </C:address-data>
  </D:prop>
$filter
</C:addressbook-query>
EOXML;

 	  $this->DoXMLRequest( 'REPORT', $xml, $relative_url );
    $xml_parser = xml_parser_create_ns('UTF-8');
    $this->xml_tags = array();
    xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
    xml_parse_into_struct( $xml_parser, $this->xmlResponse, $this->xml_tags );
    xml_parser_free($xml_parser);

    $report = array();
    foreach( $this->xml_tags as $k => $v ) {
      switch( $v['tag'] ) {
        case 'DAV::RESPONSE':
          if ( $v['type'] == 'open' ) {
            $response = array();
          }
          elseif ( $v['type'] == 'close' ) {
            $report[] = $response;
          }
          break;
        case 'DAV::HREF':
          $response['href'] = basename( $v['value'] );
          break;
        case 'DAV::GETETAG':
          $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
          break;
        case 'URN:IETF:PARAMS:XML:NS:CARDDAV:ADDRESS-DATA':
          $response['data'] = $v['value'];
          break;
      }
    }
    return $report;
  }

function GetVcards( $start = null, $finish = null, $relative_url = '' ) {    $filter = "";
    if ( isset($start) && isset($finish) )
        $range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
    else
        $range = '';

    $filter = <<<EOFILTER
  <C:filter>
    <C:comp-filter name="VCARD">
      $range
    </C:comp-filter>
  </C:filter>
EOFILTER;

    return $this->DoAddressbookQuery($filter, $relative_url);
  }



  /**
  * Given XML for a calendar query, return an array of the events (/todos) in the
  * response.  Each event in the array will have a 'href', 'etag' and '$response_type'
  * part, where the 'href' is relative to the calendar and the '$response_type' contains the
  * definition of the calendar data in iCalendar format.
  *
  * @param string $filter XML fragment which is the <filter> element of a calendar-query
  * @param string $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
  * @param string $report_type Used as a name for the array element containing the calendar data. @deprecated
  *
  * @return array An array of the relative URLs, etags, and events from the server.  Each element of the array will
  *               be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied
  *               etag (which only varies when the data changes) and the calendar data in iCalendar format.
  */
  function DoCalendarQuery( $filter, $relative_url = '' ) {

    $xml = <<<EOXML
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
  <D:prop xmlns:D="DAV:">
    <C:calendar-data/>
    <D:getetag/>
  </D:prop>$filter
</C:calendar-query>
EOXML;

$etag_xml_tags = NULL;

if($this->type == BEDIWORK_DAV_SERVER)
{
    $xml = <<<EOXML
<?xml version="1.0" encoding="UTF-8"?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  <D:prop>
    <D:getetag/>
  </D:prop>
$filter
</C:calendar-query>
EOXML;

    $this->DoXMLRequest( 'REPORT', $xml, $relative_url );
    $xml_parser = xml_parser_create_ns('UTF-8');
    $etag_xml_tags = array();
    xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
    xml_parse_into_struct( $xml_parser, $this->xmlResponse, $etag_xml_tags );
    xml_parser_free($xml_parser);
    $etag_report = array();

		    foreach( $etag_xml_tags as $k => $v ) {
		 	$pos++;
		      switch( $v['tag'] ) {
		        case 'DAV::RESPONSE':
		          if ( $v['type'] == 'open' ) {
		            $response = array();
		          }
		          elseif ( $v['type'] == 'close' ) {
		            $etag_report[] = $response;
		          }
		          break;
		        case 'DAV::HREF':
		          $response['href'] = basename( $v['value'] );
		          break;
		        case 'DAV::GETETAG':
		          $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
		          break;
		        case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-DATA':
		          $response['data'] = $v['value'];
		          break;
		      }
		    }

    $xml = <<<EOXML
<?xml version="1.0" encoding="UTF-8"?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-data>
<C:comp name="VCALENDAR">
<C:allprop/>
</C:comp>
</C:calendar-data>
</D:prop>
$filter
</C:calendar-query>
EOXML;

}

    $this->DoXMLRequest( 'REPORT', $xml, $relative_url );
    $xml_parser = xml_parser_create_ns('UTF-8');
    $this->xml_tags = array();
    xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
    xml_parse_into_struct( $xml_parser, $this->xmlResponse, $this->xml_tags );
    xml_parser_free($xml_parser);
	
    $report = array();
    foreach( $this->xml_tags as $k => $v ) {
      switch( $v['tag'] ) {
        case 'DAV::RESPONSE':
          if ( $v['type'] == 'open' ) {
            $response = array();
          }
          elseif ( $v['type'] == 'close' ) {
            $report[] = $response;
          }
          break;
        case 'DAV::HREF':
          $response['href'] = basename( $v['value'] );
          break;
        case 'DAV::GETETAG':
          $response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
          break;
        case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-DATA':
		$v['value'] = str_replace("\r\n ", "", $v['value']);
          $response['data'] = $v['value'];
          break;
      }
    }

	if(isset($etag_report) && $etag_report != NULL)
	{
		foreach($report as &$item)
		{
			foreach($etag_report as $etag_item)
			{
				if($item['href'] == $etag_item['href'])
				{
					$item['etag'] = $etag_item['etag'];
				}
			}		
		}
		
	}
	
    return $report;
  }

  /**
  * Get the alarms in a range from $start to $finish.  The dates should be in the
  * format yyyymmddThhmmssZ and should be in GMT.  The events are returned as an
  * array of alarm arrays.  Each alarm array will have a 'href', 'etag' and 'alarm'
  * part, where the 'href' is relative to the calendar and the event contains the
  * definition of the event in iCalendar format.
  *
  * @param timestamp $start The start time for the period
  * @param timestamp $finish The finish time for the period
  * @param string    $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
  *
  * @return array An array of the relative URLs, etags, and alarms, returned from DoCalendarQuery() @see DoCalendarQuery()
  */
  function GetAlarms( $start = null, $finish = null, $relative_url = '' ) {
    $filter = "";
    if ( isset($start) && isset($finish) )
        $range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
    else
        $range = '';

$filter = <<<EOFILTER
  <C:filter>
    <C:comp-filter name="VCALENDAR">
    <C:comp-filter name="VEVENT">
    <C:comp-filter name="VALARM">
    $range
    </C:comp-filter>
    </C:comp-filter>
    </C:comp-filter>
  </C:filter>
EOFILTER;
	$this->SetDepth(1);
    return $this->DoCalendarQuery($filter, $relative_url);
  }

  /**
  * Get the events in a range from $start to $finish.  The dates should be in the
  * format yyyymmddThhmmssZ and should be in GMT.  The events are returned as an
  * array of event arrays.  Each event array will have a 'href', 'etag' and 'event'
  * part, where the 'href' is relative to the calendar and the event contains the
  * definition of the event in iCalendar format.
  *
  * @param timestamp $start The start time for the period
  * @param timestamp $finish The finish time for the period
  * @param string    $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
  *
  * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
  */
  function GetEvents( $start = null, $finish = null, $relative_url = '' ) {
    $filter = "";
    if ( isset($start) && isset($finish) )
        $range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
    else
        $range = '';

$filter = <<<EOFILTER
  <C:filter>
    <C:comp-filter name="VCALENDAR">
    <C:comp-filter name="VEVENT">
    $range
    </C:comp-filter>
    </C:comp-filter>
  </C:filter>
EOFILTER;
	$this->SetDepth(1);
    return $this->DoCalendarQuery($filter, $relative_url);
  }


  /**
  * Get the todo's in a range from $start to $finish.  The dates should be in the
  * format yyyymmddThhmmssZ and should be in GMT.  The events are returned as an
  * array of event arrays.  Each event array will have a 'href', 'etag' and 'event'
  * part, where the 'href' is relative to the calendar and the event contains the
  * definition of the event in iCalendar format.
  *
  * @param timestamp $start The start time for the period
  * @param timestamp $finish The finish time for the period
  * @param boolean   $completed Whether to include completed tasks
  * @param boolean   $cancelled Whether to include cancelled tasks
  * @param string    $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
  *
  * @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
  */
  function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = "" ) {

    if ( isset($start) && isset($finish) && $start == null && $finish == null )
        $range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
    else
        $range = '';

    // Warning!  May contain traces of double negatives...
    $neg_cancelled = ( $cancelled === true ? "no" : "yes" );
    $neg_completed = ( $cancelled === true ? "no" : "yes" );

		if ( isset($completed) && isset($cancelled) && $completed != null && $cancelled != null)
		{
$filters = <<<EOFILTER
<C:prop-filter name="STATUS">
	<C:text-match negate-condition="$neg_completed">COMPLETED</C:text-match>
</C:prop-filter>
<C:prop-filter name="STATUS">
	<C:text-match negate-condition="$neg_cancelled">CANCELLED</C:text-match>
</C:prop-filter>
EOFILTER;
		}
		else
			$filters = '';

$filter = <<<EOFILTER
  <C:filter>
    <C:comp-filter name="VCALENDAR">
      <C:comp-filter name="VTODO">$filters$range
      </C:comp-filter>
    </C:comp-filter>
  </C:filter>
EOFILTER;
	$this->SetDepth(1);
    return $this->DoCalendarQuery($filter, $relative_url);
  }


  /**
  * Get the calendar entry by UID
  *
  * @param uid
  * @param string    $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
  *
  * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery()
  */
  function GetEntryByUid( $uid, $relative_url = '' ) {
    $filter = "";
    if ( $uid ) {
      $filter = <<<EOFILTER
  <C:filter>
    <C:comp-filter name="VCALENDAR">
          <C:comp-filter name="VEVENT">
                <C:prop-filter name="UID">
                        <C:text-match icollation="i;octet">$uid</C:text-match>
                </C:prop-filter>
          </C:comp-filter>
    </C:comp-filter>
  </C:filter>
EOFILTER;
    }

    return $this->DoCalendarQuery($filter, $relative_url);
  }

	function SearchEntry($searches, $relative_url)
	{
	$filters = "";
	foreach($searches as $search)
	{
		$property = $search['property'];
		$search_field= $search['search'];
		$field = $search['test'];

		$filters = $filters . '<C:prop-filter name="' . $property . '"><C:text-match collation="i;unicode-casemap" match-type="' . $search_field . '">' . $field . '</C:text-match></C:prop-filter>';
	}
	
      $filter = <<<EOFILTER
  <C:filter>
	$filters
  </C:filter>
EOFILTER;

    return $this->DoAddressbookQuery($filter, $relative_url);

	}

  /**
  * Get the calendar entry by UID
  *
  * @param uid
  * @param string    $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
  *
  * @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery()
  */
  function GetEntryByUidEx( $uid, $type, $relative_url = '' ) {
    $filter = "";
    if ( $uid ) {
      $filter = <<<EOFILTER
  <C:filter>
    <C:comp-filter name="VCALENDAR">
          <C:comp-filter name="$type">
                <C:prop-filter name="UID">
                        <C:text-match icollation="i;octet">$uid</C:text-match>
                </C:prop-filter>
          </C:comp-filter>
    </C:comp-filter>
  </C:filter>
EOFILTER;
    }

    return $this->DoCalendarQuery($filter, $relative_url);
  }


  /**
  * Get the calendar entry by HREF
  *
  * @param string    $href         The href from a call to GetEvents or GetTodos etc.
  * @param string    $relative_url The URL relative to the base_url specified when the calendar was opened.  Default ''.
  *
  * @return string The iCalendar of the calendar entry
  */
  function GetEntryByHref( $href, $relative_url = '' ) {
    return $this->DoGETRequest( $relative_url . $href );
  }



function generate_ctag()
{
	return date('Y-m-d H:i:s.u' . rand(0, 9));
}

	function setupNewUri( $uri, $principal_uid)
	{
$xml = <<<EOXML
<?xml version='1.0' encoding='UTF-8'?><resourcetype xmlns='DAV:'>
  <collection/>
  <calendar xmlns='urn:ietf:params:xml:ns:caldav'/>
</resourcetype>
EOXML;
		$this->setXattr( $uri . '/calendar/', 'DAV:', 'resourcetype', $xml);
		
		$xml = "<?xml version='1.0' encoding='UTF-8'?><getctag xmlns='http://calendarserver.org/ns/'>" . $this->generate_ctag() . "</getctag>";
		$this->setXattr( $uri . '/calendar/', 'http://calendarserver.org/ns/', 'getctag', $xml);

$xml = <<<EOXML
<?xml version='1.0' encoding='UTF-8'?><schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'>
  <opaque/>
</schedule-calendar-transp>
EOXML;
		$this->setXattr( $uri . '/calendar/', 'urn:ietf:params:xml:ns:caldav', 'schedule-calendar-transp', $xml);
				
		$xml = "<?xml version='1.0' encoding='UTF-8'?><getctag xmlns='http://calendarserver.org/ns/'>" . $this->generate_ctag() . "</getctag>";
		$this->setXattr( $uri . '/inbox/', 'http://calendarserver.org/ns/', 'getctag', $xml);

$xml = <<<EOXML
<?xml version='1.0' encoding='UTF-8'?><calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>
  <href xmlns='DAV:'>/calendars/__uids__/$principal_uid/calendar</href>
</calendar-free-busy-set>
EOXML;
		$this->setXattr( $uri . '/inbox/', 'urn:ietf:params:xml:ns:caldav', 'calendar-free-busy-set', $xml);

$xml = <<<EOXML
<?xml version='1.0' encoding='UTF-8'?><schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'>
  <href xmlns='DAV:'>/calendars/__uids__/$principal_uid/calendar</href>
</schedule-default-calendar-URL>
EOXML;
		$this->setXattr( $uri . '/inbox/', 'urn:ietf:params:xml:ns:caldav', 'schedule-default-calendar-URL', $xml);
	}

	function updateUserQuota( $uri )
	{
        throw new Atmail_Exception('This API is no longer needed or functional');
        /*
		if(is_dir($uri))
		{
			$current_quota_used = $this->getDirectorySize( $uri );
			$xml = "<?xml version='1.0' encoding='UTF-8'?><quota-used xmlns='http://twistedmatrix.com/xml_namespace/dav/private/'>" . $current_quota_used['size'] . "</quota-used>";
			return $this->setXattr( $uri, 'http://twistedmatrix.com/xml_namespace/dav/private/', 'quota-used', $xml);
		}
		return false;*/
	}
	
	function updateIcs( $uri )
	{		
		$xml = "<?xml version='1.0' encoding='UTF-8'?><getcontenttype xmlns='DAV:'>text/calendar</getcontenttype>";
		$this->setXattr( $uri, 'DAV:', 'getcontenttype', $xml);

		$xml = "<?xml version='1.0' encoding='UTF-8'?><getcontentmd5 xmlns='http://twistedmatrix.com/xml_namespace/dav/'>" . md5(file_get_contents($uri)) . "</getcontentmd5>";
		$this->setXattr( $uri, 'http://twistedmatrix.com/xml_namespace/dav/', 'getcontentmd5', $xml);

		$xml = "<?xml version='1.0' encoding='UTF-8'?><scheduling-object-resource xmlns='http://twistedmatrix.com/xml_namespace/dav/private/'>false</scheduling-object-resource>";
		$this->setXattr( $uri, 'http://twistedmatrix.com/xml_namespace/dav/private/', 'scheduling-object-resource', $xml);
	}
	
	function myUrlencode($string)
	{
	    return str_replace("%3A", ":", urlencode($string));
	}
	
	function setXattr($uri, $namespace, $property, $value)
	{
		// safe = {}:
		if(!function_exists('xattr_set') || !function_exists('http_deflate'))
		{
			die("You are currently missing the required dependancies for raw ics writing. Please install the packages below and retry. php-pear, curl-devel, libattr-devel, zlib-devel, pecl::pecl_http, pecl::xattr, libattr-devel");
		}
		return xattr_set($uri, '{'. $this->myUrlencode($namespace) . '}' . $this->myUrlencode($property), http_deflate($value));
	}
}

