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