<?php


/**
 * Classe de gestion du serveur Caldav
 * Les méthodes suivantes sont utilisables comme des callbacks a définir avant le start();
 * calendarLastUpdate(user,calendar) : doit retourner le timestamp de la derniere modification sur le calendrier ciblé (le client s'en sert pour la gestion des performances)
 * searchEvents(user,calendar) : doit retourner un tableau d'évenements sur le calendrier ciblé
 * saveEvent(user,calendar,event,infos) : doit effectuer une sauvegarde de l'évenement ciblé
 * deleteEvent(user,calendar,event) : doit effectuer une supression de l'évenement ciblé
 * @author valentin carruesco
 * @category Planning
 * @license cc by nc sa
 */

class CalDavServer{
	public $root,$searchEvents,$deleteEvent,$saveEvent,$calendarLastUpdate,$log;



	public function start(){

		$server = $_SERVER;


		$method = strtoupper($server['REQUEST_METHOD']);
		$body = stream_get_contents(fopen('php://input','r+'));


		$user = 'default';
		$calendar = 'default';

		$target = str_replace($this->root,'',$server['REQUEST_URI']);
		$infos = explode('/',$target);
		if(isset($infos[0]))$user = $infos[0];
		if(isset($infos[1])) $calendar = $infos[1];

		$logFunction  = $this->log;
		if(isset($logFunction)) $logFunction('notice','receive : '.$method.', body : '.PHP_EOL.$body.PHP_EOL.PHP_EOL.' --- target : '.$server['REQUEST_URI'].PHP_EOL.PHP_EOL);


		switch($method) {

			//Le client veux la liste des évenements de $calendar pour le user $user au format ical
			case 'REPORT':

			$stream = '<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">';

			$callback = $this->searchEvents;
			$events = $callback($user,$calendar);
			if(isset($logFunction)) $logFunction('notice','fetch '.count($events).' events, parsing to xml ...'.PHP_EOL.PHP_EOL);

			foreach($events as $event){
				$stream .= '<d:response>
				<d:href>'.$this->root.$user.'/'.$calendar.'/'.$event['id'].'.ics</d:href>
				<d:propstat>
				<d:prop>
				<cal:calendar-data>';
				$stream .= IcalEvent::toString($event);
				$stream .='</cal:calendar-data>
				<d:getetag>'.$event['updated'].'</d:getetag>
				</d:prop>
				<d:status>HTTP/1.1 200 OK</d:status>
				</d:propstat>
				</d:response>';
			}

			$stream .='</d:multistatus>';

			if(isset($logFunction)) $logFunction('notice','return stream : '.$stream.' '.PHP_EOL.PHP_EOL,true);

			header('HTTP/1.1 207 Multi-Status');
			echo $stream;
			exit();

			break;
			case 'PROPFIND':

		//Le client souhaite avoir le ctag (timestamp de derniere mise a jour du calendrier ciblé), si ce ctag est égal au dernier ctag fournis, le client ne met pas a jour les events
		if(strpos($body, 'getctag')!==false) {

			$callback = $this->calendarLastUpdate;
			$lastUpdate = $callback($user,$calendar);

			$stream = '<?xml version="1.0" encoding="utf-8"?>
			<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">
				<d:response>
					<d:href>'.$this->root.'</d:href>
					<d:propstat>
						<d:prop>
							<d:current-user-principal>
								<d:href>'.$this->root.$user.'</d:href>
							</d:current-user-principal>
							<d:owner>
								<d:href>'.$this->root.$user.'</d:href>
							</d:owner>
							<cal:supported-calendar-component-set>
								<cal:comp name="VEVENT"/>
								<cal:comp name="VTODO"/>
							</cal:supported-calendar-component-set>
							<cs:getctag>'.$lastUpdate.'</cs:getctag>
							<d:resourcetype>
								<d:collection/>
								<cal:calendar/>
							</d:resourcetype>
							<d:supported-report-set>
								<d:supported-report>
									<d:report>
										<d:expand-property/>
									</d:report>
								</d:supported-report>
								<d:supported-report>
									<d:report>
										<d:principal-property-search/>
									</d:report>
								</d:supported-report>
								<d:supported-report>
									<d:report>
										<d:principal-search-property-set/>
									</d:report>
								</d:supported-report>
								<d:supported-report>
									<d:report>
										<cal:calendar-multiget/>
									</d:report>
								</d:supported-report>
								<d:supported-report>
									<d:report>
										<cal:calendar-query/>
									</d:report>
								</d:supported-report>
								<d:supported-report>
									<d:report>
										<cal:free-busy-query/>
									</d:report>
								</d:supported-report>
							</d:supported-report-set>
						</d:prop>
						<d:status>HTTP/1.1 200 OK</d:status>
					</d:propstat>
				</d:response>
			</d:multistatus>';
		}

		//pour le calenderier ciblé, le client souhaite voir tous les etag (tag de derniere maj d'un évenement) pour voir lesquels il doit demander au serveur
		if(strpos($body, 'getetag')!==false) {

				$f = $this->searchEvents;
				$events = $f($user,$calendar);

				$stream = '<?xml version="1.0" encoding="utf-8"?>
				<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">
				<d:response>
				<d:href>'.$this->root.$user.'/'.$calendar.'/</d:href>
				<d:propstat>
				<d:prop>
				<d:resourcetype>
				<d:collection/>
				<cal:calendar/>
				</d:resourcetype>
				</d:prop>
				<d:status>HTTP/1.1 200 OK</d:status>
				</d:propstat>
				<d:propstat>
				<d:prop>
				<d:getcontenttype/>
				<d:getetag/>
				</d:prop>
				<d:status>HTTP/1.1 404 Not Found</d:status>
				</d:propstat>
				</d:response>';

				foreach($events as $event){
					$stream .= '<d:response>
					<d:href>'.$this->root.$user.'/'.$calendar.'/'.$event['id'].'.ics</d:href>
					<d:propstat>
					<d:prop>
					<d:getcontenttype>text/calendar; charset=utf-8</d:getcontenttype>
					<d:resourcetype/>
					<d:getetag>'.$event['updated'].'</d:getetag>
					</d:prop>
					<d:status>HTTP/1.1 200 OK</d:status>
					</d:propstat>
					</d:response>';
				}
				$stream .= '</d:multistatus>';
			}

			if(isset($logFunction)) $logFunction('notice','return stream : '.$stream.' '.PHP_EOL.PHP_EOL,true);

			header('HTTP/1.1 207 Multi-Status');

			echo $stream;
			exit();
			break;

      //Récuperation d'un fichier (ou dossier ?)
			case 'GET':
			header('HTTP/1.1 200 Ok');

			break;

      //Envois d'un evenement
			case 'PUT':
			header('HTTP/1.1 201 Created');



			preg_match('|([0-9]*)\.ics|i',$server['REQUEST_URI'],$m);


			$callback = $this->saveEvent;
			$events = $callback($user,$calendar,$m[1],IcalEvent::fromString($body));

			if(isset($logFunction)) $logFunction('notice','event saved '.$user.'/'.$calendar.', id: '.$m[1].PHP_EOL.PHP_EOL);

			break;

      //Supression dossier/fichier
			case 'DELETE':

			header('HTTP/1.1 200 Ok');
			$callback = $this->deleteEvent;

			preg_match('|([0-9]*)\.ics|i',$server['REQUEST_URI'],$m);
			try{
				if(!isset($m[1])) throw new Exception("Event id non récuperable: ".$server['REQUEST_URI']);

				$callback($user,$calendar,$m[1]);

				if(isset($logFunction)) $logFunction('notice','event deleted '.$user.'/'.$calendar.', id: '.$m[1].PHP_EOL.PHP_EOL);

				header('HTTP/1.1 200 Ok');
			}catch(Exeption $e){
				if(isset($logFunction)) $logFunction('error','unable to delete event: '.$e->getMessage().' '.PHP_EOL.PHP_EOL);
				header('HTTP/1.1 403 Forbidden');
			}
      /*

       200 => 'Ok',
        201 => 'Created',
        204 => 'No Content',
        207 => 'Multi-Status',
        403 => 'Forbidden',
        404 => 'Not Found',
        409 => 'Conflict',
        415 => 'Unsupported Media Type',
        500 => 'Internal Server Error',
        501 => 'Method not implemented',
     );

     return 'HTTP/1.1 ' . $code . ' ' . $msg[$code];


      */

     break;

      //Déplacement/renommage dossier/fichier
     case 'MOVE':

     header('HTTP/1.1 200 Ok');


     break;

      //The OPTIONS method allows an http client to find out what HTTP methods are supported on a specific url.
     case 'OPTIONS':
     header('Allows: options get head post delete trace propfind proppatch copy mkcol put');


     break;
     case 'HEAD':
     header('HTTP/1.1 200 Ok');
        //header('HTTP/1.1 501 Method not implemented');
     break;
     case 'POST':
     header('HTTP/1.1 501 Method not implemented');
     break;
     case 'TRACE':
     header('HTTP/1.1 501 Method not implemented');
     break;
      //Updates properties of a resource or collection.
     case 'PROPPATCH':
     header('HTTP/1.1 501 Method not implemented');
     break;
      //Copie d'un élement vers un nouvel emplacement
     case 'COPY':
        //header('HTTP/1.1 501 Method not implemented');
     header('HTTP/1.1 200 Ok');

     break;
      //Verouillage d'un élement
     case 'LOCK':
     header('HTTP/1.1 501 Method not implemented');
     break;
      //Déverouillage d'un élement
     case 'UNLOCK':
     header('HTTP/1.1 501 Method not implemented');
     break;

 }

}
}




class IcalEvent{
	public $title,$description,$start,$end,$frequency,$location,$categories,$alarms,$ics,$uid;

	public static function fromString($ical){
		$event = new self();
		$lines = array();
		foreach(explode("\n",$ical) as $line):
			$columns = explode(":",trim($line));
			if(!isset($columns[1])) continue;
			$key = $columns[0];
			$value = $columns[1];

			$keyvalues = explode(';',$key);
			$key = array_shift($keyvalues);
			//Ne prendre que la premiere description
			if($key=='DESCRIPTION' && isset($lines['DESCRIPTION'])) continue;
			$lines[$key] = $value;
		endforeach;

		if(isset($lines['CATEGORIES']))  $event->categories = $lines['CATEGORIES'];
		if(isset($lines['SUMMARY']))  $event->title = $lines['SUMMARY'];
		if(isset($lines['DESCRIPTION']))  $event->description = $lines['DESCRIPTION'];
		if(isset($lines['DTSTART']))  	  $event->start = strtotime($lines['DTSTART']);
		if(isset($lines['DTEND']))  $event->end = strtotime($lines['DTEND']);
		if(isset($lines['RRULE']))  $event->frequency = $lines['RRULE'];
		if(isset($lines['LOCATION']))  $event->location = $lines['LOCATION'];
		if(isset($lines['UID']))  $event->uid = $lines['UID'];
		if(isset($lines['TRIGGER'])) $event->alarms = $lines['TRIGGER'];
		return $event;
	}

	public static function toString($event){
		$ics = 'BEGIN:VCALENDAR'."\n";
		$ics .= 'PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN'."\n";
		$ics .= 'VERSION:2.0'."\n";
		$ics .= 'BEGIN:VTIMEZONE'."\n";
		$ics .= 'TZID:Europe/Paris'."\n";
		$ics .= 'BEGIN:DAYLIGHT'."\n";
		$ics .= 'TZOFFSETFROM:+0100'."\n";
		$ics .= 'TZOFFSETTO:+0200'."\n";
		$ics .= 'TZNAME:CEST'."\n";
		$ics .= 'END:DAYLIGHT'."\n";
		$ics .= 'BEGIN:STANDARD'."\n";
		$ics .= 'TZOFFSETFROM:+0200'."\n";
		$ics .= 'TZOFFSETTO:+0100'."\n";
		$ics .= 'TZNAME:CET'."\n";
		$ics .= 'DTSTART:19701025T030000'."\n";
		$ics .= 'END:STANDARD'."\n";
		$ics .= 'END:VTIMEZONE'."\n";

		$ics .= 'BEGIN:VEVENT'."\n";

		$ics .= 'DTSTART:19700329T020000'."\n";
		if(isset($event['recurrence']) && $event['recurrence']!='')
			$ics .= 'RRULE:'.$event['recurrence']."\n";

		$ics .= 'CREATED:'.date('Ymd',$event['created']).'T'.date('His',$event['created']).'Z'."\n";
		$ics .= 'LAST-MODIFIED:'.date('Ymd',$event['updated']).'T'.date('His',$event['updated']).'Z'."\n";
		$ics .= 'DTSTAMP:20181209T222306Z'."\n";
		$ics .= 'UID:'.$event['id']."\n";

		if(!empty($event['label']))
		$ics .= 'SUMMARY:'.$event['label']."\n";

		if(!empty($event['type']))
		$ics .= 'CATEGORIES:'.$event['type']."\n";

		if(!empty($event['description']))
			$ics .= 'DESCRIPTION:'.str_replace(array(PHP_EOL,"\r","\n"),'\n',$event['description'])." \n";

		if($event['street'].$event['zip'].$event['city']!='')
			$ics .= 'LOCATION:'.$event['street'].' '.$event['zip'].' '.$event['city']."\n";

		$ics .= 'DTSTART;TZID=Europe/Paris:'.date('Ymd',$event['startDate']).'T'.date('His',$event['startDate'])."\n";
		$ics .= 'DTEND;TZID=Europe/Paris:'.date('Ymd',$event['endDate']).'T'.date('His',$event['endDate'])."\n";
		$ics .= 'TRANSP:OPAQUE'."\n"; //TRANSP : Définit si la ressource affectée à l'événement est rendu indisponible (OPAQUE, TRANSPARENT)
		if($event['notificationNumber']!='0'){
			if($event['notificationUnity'] == 'minut') $notification= 'PT'.$event['notificationNumber'].'M';
			if($event['notificationUnity'] == 'hour') $notification = 'PT'.$event['notificationNumber'].'H';
			if($event['notificationUnity'] == 'day') $notification = 'P'.$event['notificationNumber'].'D';
			$ics .= 'BEGIN:VALARM'."\n";
			$ics .= 'ACTION:DISPLAY'."\n";
			$ics .= 'TRIGGER;VALUE=DURATION:-'.$notification.''."\n";
			$ics .= 'DESCRIPTION:Rappel erp'."\n";
			$ics .= 'END:VALARM'."\n";
		}
		$ics .= 'STATUS:CONFIRMED'."\n"; //STATUS : Statut de l'événement (TENTATIVE, CONFIRMED, CANCELLED)

		$ics .= 'END:VEVENT'."\n";
		$ics .= 'END:VCALENDAR'."\n";
		return $ics;
	}
}

?>