CalDavServer.class.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. <?php
  2. /**
  3. * Classe de gestion du serveur Caldav
  4. * Les méthodes suivantes sont utilisables comme des callbacks a définir avant le start();
  5. * 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)
  6. * searchEvents(user,calendar) : doit retourner un tableau d'évenements sur le calendrier ciblé
  7. * saveEvent(user,calendar,event,infos) : doit effectuer une sauvegarde de l'évenement ciblé
  8. * deleteEvent(user,calendar,event) : doit effectuer une supression de l'évenement ciblé
  9. * @author valentin carruesco
  10. * @category Planning
  11. * @license cc by nc sa
  12. */
  13. class CalDavServer{
  14. public $root,$searchEvents,$deleteEvent,$saveEvent,$calendarLastUpdate,$log;
  15. public function start(){
  16. $server = $_SERVER;
  17. $method = strtoupper($server['REQUEST_METHOD']);
  18. $body = stream_get_contents(fopen('php://input','r+'));
  19. $user = 'default';
  20. $calendar = 'default';
  21. $target = str_replace($this->root,'',$server['REQUEST_URI']);
  22. $infos = explode('/',$target);
  23. if(isset($infos[0]))$user = $infos[0];
  24. if(isset($infos[1])) $calendar = $infos[1];
  25. $logFunction = $this->log;
  26. if(isset($logFunction)) $logFunction('notice','receive : '.$method.', body : '.PHP_EOL.$body.PHP_EOL.PHP_EOL.' --- target : '.$server['REQUEST_URI'].PHP_EOL.PHP_EOL);
  27. switch($method) {
  28. //Le client veux la liste des évenements de $calendar pour le user $user au format ical
  29. case 'REPORT':
  30. $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/">';
  31. $callback = $this->searchEvents;
  32. $events = $callback($user,$calendar);
  33. if(isset($logFunction)) $logFunction('notice','fetch '.count($events).' events, parsing to xml ...'.PHP_EOL.PHP_EOL);
  34. foreach($events as $event){
  35. $stream .= '<d:response>
  36. <d:href>'.$this->root.$user.'/'.$calendar.'/'.$event['id'].'.ics</d:href>
  37. <d:propstat>
  38. <d:prop>
  39. <cal:calendar-data>';
  40. $stream .= IcalEvent::toString($event);
  41. $stream .='</cal:calendar-data>
  42. <d:getetag>'.$event['updated'].'</d:getetag>
  43. </d:prop>
  44. <d:status>HTTP/1.1 200 OK</d:status>
  45. </d:propstat>
  46. </d:response>';
  47. }
  48. $stream .='</d:multistatus>';
  49. if(isset($logFunction)) $logFunction('notice','return stream : '.$stream.' '.PHP_EOL.PHP_EOL,true);
  50. header('HTTP/1.1 207 Multi-Status');
  51. echo $stream;
  52. exit();
  53. break;
  54. case 'PROPFIND':
  55. //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
  56. if(strpos($body, 'getctag')!==false) {
  57. $callback = $this->calendarLastUpdate;
  58. $lastUpdate = $callback($user,$calendar);
  59. $stream = '<?xml version="1.0" encoding="utf-8"?>
  60. <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/">
  61. <d:response>
  62. <d:href>'.$this->root.'</d:href>
  63. <d:propstat>
  64. <d:prop>
  65. <d:current-user-principal>
  66. <d:href>'.$this->root.$user.'</d:href>
  67. </d:current-user-principal>
  68. <d:owner>
  69. <d:href>'.$this->root.$user.'</d:href>
  70. </d:owner>
  71. <cal:supported-calendar-component-set>
  72. <cal:comp name="VEVENT"/>
  73. <cal:comp name="VTODO"/>
  74. </cal:supported-calendar-component-set>
  75. <cs:getctag>'.$lastUpdate.'</cs:getctag>
  76. <d:resourcetype>
  77. <d:collection/>
  78. <cal:calendar/>
  79. </d:resourcetype>
  80. <d:supported-report-set>
  81. <d:supported-report>
  82. <d:report>
  83. <d:expand-property/>
  84. </d:report>
  85. </d:supported-report>
  86. <d:supported-report>
  87. <d:report>
  88. <d:principal-property-search/>
  89. </d:report>
  90. </d:supported-report>
  91. <d:supported-report>
  92. <d:report>
  93. <d:principal-search-property-set/>
  94. </d:report>
  95. </d:supported-report>
  96. <d:supported-report>
  97. <d:report>
  98. <cal:calendar-multiget/>
  99. </d:report>
  100. </d:supported-report>
  101. <d:supported-report>
  102. <d:report>
  103. <cal:calendar-query/>
  104. </d:report>
  105. </d:supported-report>
  106. <d:supported-report>
  107. <d:report>
  108. <cal:free-busy-query/>
  109. </d:report>
  110. </d:supported-report>
  111. </d:supported-report-set>
  112. </d:prop>
  113. <d:status>HTTP/1.1 200 OK</d:status>
  114. </d:propstat>
  115. </d:response>
  116. </d:multistatus>';
  117. }
  118. //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
  119. if(strpos($body, 'getetag')!==false) {
  120. $f = $this->searchEvents;
  121. $events = $f($user,$calendar);
  122. $stream = '<?xml version="1.0" encoding="utf-8"?>
  123. <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/">
  124. <d:response>
  125. <d:href>'.$this->root.$user.'/'.$calendar.'/</d:href>
  126. <d:propstat>
  127. <d:prop>
  128. <d:resourcetype>
  129. <d:collection/>
  130. <cal:calendar/>
  131. </d:resourcetype>
  132. </d:prop>
  133. <d:status>HTTP/1.1 200 OK</d:status>
  134. </d:propstat>
  135. <d:propstat>
  136. <d:prop>
  137. <d:getcontenttype/>
  138. <d:getetag/>
  139. </d:prop>
  140. <d:status>HTTP/1.1 404 Not Found</d:status>
  141. </d:propstat>
  142. </d:response>';
  143. foreach($events as $event){
  144. $stream .= '<d:response>
  145. <d:href>'.$this->root.$user.'/'.$calendar.'/'.$event['id'].'.ics</d:href>
  146. <d:propstat>
  147. <d:prop>
  148. <d:getcontenttype>text/calendar; charset=utf-8</d:getcontenttype>
  149. <d:resourcetype/>
  150. <d:getetag>'.$event['updated'].'</d:getetag>
  151. </d:prop>
  152. <d:status>HTTP/1.1 200 OK</d:status>
  153. </d:propstat>
  154. </d:response>';
  155. }
  156. $stream .= '</d:multistatus>';
  157. }
  158. if(isset($logFunction)) $logFunction('notice','return stream : '.$stream.' '.PHP_EOL.PHP_EOL,true);
  159. header('HTTP/1.1 207 Multi-Status');
  160. echo $stream;
  161. exit();
  162. break;
  163. //Récuperation d'un fichier (ou dossier ?)
  164. case 'GET':
  165. header('HTTP/1.1 200 Ok');
  166. break;
  167. //Envois d'un evenement
  168. case 'PUT':
  169. header('HTTP/1.1 201 Created');
  170. preg_match('|([0-9]*)\.ics|i',$server['REQUEST_URI'],$m);
  171. $callback = $this->saveEvent;
  172. $events = $callback($user,$calendar,$m[1],IcalEvent::fromString($body));
  173. if(isset($logFunction)) $logFunction('notice','event saved '.$user.'/'.$calendar.', id: '.$m[1].PHP_EOL.PHP_EOL);
  174. break;
  175. //Supression dossier/fichier
  176. case 'DELETE':
  177. header('HTTP/1.1 200 Ok');
  178. $callback = $this->deleteEvent;
  179. preg_match('|([0-9]*)\.ics|i',$server['REQUEST_URI'],$m);
  180. try{
  181. if(!isset($m[1])) throw new Exception("Event id non récuperable: ".$server['REQUEST_URI']);
  182. $callback($user,$calendar,$m[1]);
  183. if(isset($logFunction)) $logFunction('notice','event deleted '.$user.'/'.$calendar.', id: '.$m[1].PHP_EOL.PHP_EOL);
  184. header('HTTP/1.1 200 Ok');
  185. }catch(Exeption $e){
  186. if(isset($logFunction)) $logFunction('error','unable to delete event: '.$e->getMessage().' '.PHP_EOL.PHP_EOL);
  187. header('HTTP/1.1 403 Forbidden');
  188. }
  189. /*
  190. 200 => 'Ok',
  191. 201 => 'Created',
  192. 204 => 'No Content',
  193. 207 => 'Multi-Status',
  194. 403 => 'Forbidden',
  195. 404 => 'Not Found',
  196. 409 => 'Conflict',
  197. 415 => 'Unsupported Media Type',
  198. 500 => 'Internal Server Error',
  199. 501 => 'Method not implemented',
  200. );
  201. return 'HTTP/1.1 ' . $code . ' ' . $msg[$code];
  202. */
  203. break;
  204. //Déplacement/renommage dossier/fichier
  205. case 'MOVE':
  206. header('HTTP/1.1 200 Ok');
  207. break;
  208. //The OPTIONS method allows an http client to find out what HTTP methods are supported on a specific url.
  209. case 'OPTIONS':
  210. header('Allows: options get head post delete trace propfind proppatch copy mkcol put');
  211. break;
  212. case 'HEAD':
  213. header('HTTP/1.1 200 Ok');
  214. //header('HTTP/1.1 501 Method not implemented');
  215. break;
  216. case 'POST':
  217. header('HTTP/1.1 501 Method not implemented');
  218. break;
  219. case 'TRACE':
  220. header('HTTP/1.1 501 Method not implemented');
  221. break;
  222. //Updates properties of a resource or collection.
  223. case 'PROPPATCH':
  224. header('HTTP/1.1 501 Method not implemented');
  225. break;
  226. //Copie d'un élement vers un nouvel emplacement
  227. case 'COPY':
  228. //header('HTTP/1.1 501 Method not implemented');
  229. header('HTTP/1.1 200 Ok');
  230. break;
  231. //Verouillage d'un élement
  232. case 'LOCK':
  233. header('HTTP/1.1 501 Method not implemented');
  234. break;
  235. //Déverouillage d'un élement
  236. case 'UNLOCK':
  237. header('HTTP/1.1 501 Method not implemented');
  238. break;
  239. }
  240. }
  241. }
  242. class IcalEvent{
  243. public $title,$description,$start,$end,$frequency,$location,$categories,$alarms,$ics,$uid;
  244. public static function fromString($ical){
  245. $event = new self();
  246. $lines = array();
  247. foreach(explode("\n",$ical) as $line):
  248. $columns = explode(":",trim($line));
  249. if(!isset($columns[1])) continue;
  250. $key = $columns[0];
  251. $value = $columns[1];
  252. $keyvalues = explode(';',$key);
  253. $key = array_shift($keyvalues);
  254. //Ne prendre que la premiere description
  255. if($key=='DESCRIPTION' && isset($lines['DESCRIPTION'])) continue;
  256. $lines[$key] = $value;
  257. endforeach;
  258. if(isset($lines['CATEGORIES'])) $event->categories = $lines['CATEGORIES'];
  259. if(isset($lines['SUMMARY'])) $event->title = $lines['SUMMARY'];
  260. if(isset($lines['DESCRIPTION'])) $event->description = $lines['DESCRIPTION'];
  261. if(isset($lines['DTSTART'])) $event->start = strtotime($lines['DTSTART']);
  262. if(isset($lines['DTEND'])) $event->end = strtotime($lines['DTEND']);
  263. if(isset($lines['RRULE'])) $event->frequency = $lines['RRULE'];
  264. if(isset($lines['LOCATION'])) $event->location = $lines['LOCATION'];
  265. if(isset($lines['UID'])) $event->uid = $lines['UID'];
  266. if(isset($lines['TRIGGER'])) $event->alarms = $lines['TRIGGER'];
  267. return $event;
  268. }
  269. public static function toString($event){
  270. $ics = 'BEGIN:VCALENDAR'."\n";
  271. $ics .= 'PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN'."\n";
  272. $ics .= 'VERSION:2.0'."\n";
  273. $ics .= 'BEGIN:VTIMEZONE'."\n";
  274. $ics .= 'TZID:Europe/Paris'."\n";
  275. $ics .= 'BEGIN:DAYLIGHT'."\n";
  276. $ics .= 'TZOFFSETFROM:+0100'."\n";
  277. $ics .= 'TZOFFSETTO:+0200'."\n";
  278. $ics .= 'TZNAME:CEST'."\n";
  279. $ics .= 'END:DAYLIGHT'."\n";
  280. $ics .= 'BEGIN:STANDARD'."\n";
  281. $ics .= 'TZOFFSETFROM:+0200'."\n";
  282. $ics .= 'TZOFFSETTO:+0100'."\n";
  283. $ics .= 'TZNAME:CET'."\n";
  284. $ics .= 'DTSTART:19701025T030000'."\n";
  285. $ics .= 'END:STANDARD'."\n";
  286. $ics .= 'END:VTIMEZONE'."\n";
  287. $ics .= 'BEGIN:VEVENT'."\n";
  288. $ics .= 'DTSTART:19700329T020000'."\n";
  289. if(isset($event['recurrence']) && $event['recurrence']!='')
  290. $ics .= 'RRULE:'.$event['recurrence']."\n";
  291. $ics .= 'CREATED:'.date('Ymd',$event['created']).'T'.date('His',$event['created']).'Z'."\n";
  292. $ics .= 'LAST-MODIFIED:'.date('Ymd',$event['updated']).'T'.date('His',$event['updated']).'Z'."\n";
  293. $ics .= 'DTSTAMP:20181209T222306Z'."\n";
  294. $ics .= 'UID:'.$event['id']."\n";
  295. if(!empty($event['label']))
  296. $ics .= 'SUMMARY:'.$event['label']."\n";
  297. if(!empty($event['type']))
  298. $ics .= 'CATEGORIES:'.$event['type']."\n";
  299. if(!empty($event['description']))
  300. $ics .= 'DESCRIPTION:'.str_replace(array(PHP_EOL,"\r","\n"),'\n',$event['description'])." \n";
  301. if($event['street'].$event['zip'].$event['city']!='')
  302. $ics .= 'LOCATION:'.$event['street'].' '.$event['zip'].' '.$event['city']."\n";
  303. $ics .= 'DTSTART;TZID=Europe/Paris:'.date('Ymd',$event['startDate']).'T'.date('His',$event['startDate'])."\n";
  304. $ics .= 'DTEND;TZID=Europe/Paris:'.date('Ymd',$event['endDate']).'T'.date('His',$event['endDate'])."\n";
  305. $ics .= 'TRANSP:OPAQUE'."\n"; //TRANSP : Définit si la ressource affectée à l'événement est rendu indisponible (OPAQUE, TRANSPARENT)
  306. if($event['notificationNumber']!='0'){
  307. if($event['notificationUnity'] == 'minut') $notification= 'PT'.$event['notificationNumber'].'M';
  308. if($event['notificationUnity'] == 'hour') $notification = 'PT'.$event['notificationNumber'].'H';
  309. if($event['notificationUnity'] == 'day') $notification = 'P'.$event['notificationNumber'].'D';
  310. $ics .= 'BEGIN:VALARM'."\n";
  311. $ics .= 'ACTION:DISPLAY'."\n";
  312. $ics .= 'TRIGGER;VALUE=DURATION:-'.$notification.''."\n";
  313. $ics .= 'DESCRIPTION:Rappel erp'."\n";
  314. $ics .= 'END:VALARM'."\n";
  315. }
  316. $ics .= 'STATUS:CONFIRMED'."\n"; //STATUS : Statut de l'événement (TENTATIVE, CONFIRMED, CANCELLED)
  317. $ics .= 'END:VEVENT'."\n";
  318. $ics .= 'END:VCALENDAR'."\n";
  319. return $ics;
  320. }
  321. }
  322. ?>