xcal.lib.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. <?php
  2. /* Copyright (C) 2008-2011 Laurent Destailleur <eldy@users.sourceforge.net>
  3. *
  4. * This program is free software; you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation; either version 3 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. */
  17. /**
  18. * \file htdocs/core/lib/xcal.lib.php
  19. * \brief Function to manage calendar files (vcal/ical/...)
  20. */
  21. /**
  22. * Build a file from an array of events
  23. * All input params and data must be encoded in $conf->charset_output
  24. *
  25. * @param string $format "vcal" or "ical"
  26. * @param string $title Title of export
  27. * @param string $desc Description of export
  28. * @param array $events_array Array of events ("uid","startdate","duration","enddate","title","summary","category","email","url","desc","author")
  29. * @param string $outputfile Output file
  30. * @return int < 0 if KO, Nb of events in file if OK
  31. */
  32. function build_calfile($format, $title, $desc, $events_array, $outputfile)
  33. {
  34. global $conf, $langs;
  35. dol_syslog("xcal.lib.php::build_calfile Build cal file ".$outputfile." to format ".$format);
  36. if (empty($outputfile)) {
  37. // -1 = error
  38. return -1;
  39. }
  40. $nbevents = 0;
  41. // Note: A cal file is an UTF8 encoded file
  42. $calfileh = fopen($outputfile, "w");
  43. if ($calfileh) {
  44. include_once DOL_DOCUMENT_ROOT."/core/lib/date.lib.php";
  45. $now = dol_now();
  46. $encoding = "";
  47. if ($format === "vcal") {
  48. $encoding = "ENCODING=QUOTED-PRINTABLE:";
  49. }
  50. // Print header
  51. fwrite($calfileh, "BEGIN:VCALENDAR\n");
  52. // version is always "2.0"
  53. fwrite($calfileh, "VERSION:2.0\n");
  54. fwrite($calfileh, "METHOD:PUBLISH\n");
  55. fwrite($calfileh, "PRODID:-//DOLIBARR ".DOL_VERSION."\n");
  56. fwrite($calfileh, "CALSCALE:GREGORIAN\n");
  57. fwrite($calfileh, "X-WR-CALNAME:".$encoding.format_cal($format, $title)."\n");
  58. fwrite($calfileh, "X-WR-CALDESC:".$encoding.format_cal($format, $desc)."\n");
  59. //fwrite($calfileh,"X-WR-TIMEZONE:Europe/Paris\n");
  60. if (!empty($conf->global->MAIN_AGENDA_EXPORT_CACHE) && $conf->global->MAIN_AGENDA_EXPORT_CACHE > 60) {
  61. $hh = convertSecondToTime($conf->global->MAIN_AGENDA_EXPORT_CACHE, "hour");
  62. $mm = convertSecondToTime($conf->global->MAIN_AGENDA_EXPORT_CACHE, "min");
  63. $ss = convertSecondToTime($conf->global->MAIN_AGENDA_EXPORT_CACHE, "sec");
  64. fwrite($calfileh, "X-PUBLISHED-TTL: P".$hh."H".$mm."M".$ss."S\n");
  65. }
  66. foreach ($events_array as $key => $event) {
  67. // See http://fr.wikipedia.org/wiki/ICalendar for format
  68. // See http://www.ietf.org/rfc/rfc2445.txt for RFC
  69. // TODO: avoid use extra event array, use objects direct thahtwas created before
  70. $uid = $event["uid"];
  71. $type = $event["type"];
  72. $startdate = $event["startdate"];
  73. $duration = $event["duration"];
  74. $enddate = $event["enddate"];
  75. $summary = $event["summary"];
  76. $category = $event["category"];
  77. $priority = $event["priority"];
  78. $fulldayevent = $event["fulldayevent"];
  79. $location = $event["location"];
  80. $email = $event["email"];
  81. $url = $event["url"];
  82. $transparency = $event["transparency"];
  83. $description = dol_string_nohtmltag(preg_replace("/<br[\s\/]?>/i", "\n", $event["desc"]), 0);
  84. $created = $event["created"];
  85. $modified = $event["modified"];
  86. $assignedUsers = $event["assignedUsers"];
  87. //print $fulldayevent.' '.dol_print_date($startdate, 'dayhour', 'gmt');
  88. // Format
  89. $summary = format_cal($format, $summary);
  90. $description = format_cal($format, $description);
  91. $category = format_cal($format, $category);
  92. $location = format_cal($format, $location);
  93. // Output the vCard/iCal VEVENT object
  94. /*
  95. Example from Google ical export for a 1 hour event:
  96. BEGIN:VEVENT
  97. DTSTART:20101103T120000Z
  98. DTEND:20101103T130000Z
  99. DTSTAMP:20101121T144902Z
  100. UID:4eilllcsq8r1p87ncg7vc8dbpk@google.com
  101. CREATED:20101121T144657Z
  102. DESCRIPTION:
  103. LAST-MODIFIED:20101121T144707Z
  104. LOCATION:
  105. SEQUENCE:0
  106. STATUS:CONFIRMED
  107. SUMMARY:Tâche 1 heure
  108. TRANSP:OPAQUE
  109. END:VEVENT
  110. Example from Google ical export for a 1 day event:
  111. BEGIN:VEVENT
  112. DTSTART;VALUE=DATE:20101102
  113. DTEND;VALUE=DATE:20101103
  114. DTSTAMP:20101121T144902Z
  115. UID:d09t43kcf1qgapu9efsmmo1m6k@google.com
  116. CREATED:20101121T144607Z
  117. DESCRIPTION:
  118. LAST-MODIFIED:20101121T144607Z
  119. LOCATION:
  120. SEQUENCE:0
  121. STATUS:CONFIRMED
  122. SUMMARY:Tâche 1 jour
  123. TRANSP:TRANSPARENT
  124. END:VEVENT
  125. */
  126. if ($type === "event") {
  127. $nbevents++;
  128. fwrite($calfileh, "BEGIN:VEVENT\n");
  129. fwrite($calfileh, "UID:".$uid."\n");
  130. if (!empty($email)) {
  131. fwrite($calfileh, "ORGANIZER:MAILTO:".$email."\n");
  132. fwrite($calfileh, "CONTACT:MAILTO:".$email."\n");
  133. }
  134. if (!empty($url)) {
  135. fwrite($calfileh, "URL:".$url."\n");
  136. }
  137. if (is_array($assignedUsers)) {
  138. foreach ($assignedUsers as $assignedUser) {
  139. if ($assignedUser->email === $email) {
  140. continue;
  141. }
  142. fwrite($calfileh, "ATTENDEE;RSVP=TRUE:mailto:".$assignedUser->email."\n");
  143. }
  144. }
  145. if ($created) {
  146. fwrite($calfileh, "CREATED:".dol_print_date($created, "dayhourxcard", true)."\n");
  147. }
  148. if ($modified) {
  149. fwrite($calfileh, "LAST-MODIFIED:".dol_print_date($modified, "dayhourxcard", true)."\n");
  150. }
  151. fwrite($calfileh, "SUMMARY:".$encoding.$summary."\n");
  152. fwrite($calfileh, "DESCRIPTION:".$encoding.$description."\n");
  153. if (!empty($location)) {
  154. fwrite($calfileh, "LOCATION:".$encoding.$location."\n");
  155. }
  156. if ($fulldayevent) {
  157. fwrite($calfileh, "X-FUNAMBOL-ALLDAY:1\n");
  158. }
  159. // see https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/0f262da6-c5fd-459e-9f18-145eba86b5d2
  160. if ($fulldayevent) {
  161. fwrite($calfileh, "X-MICROSOFT-CDO-ALLDAYEVENT:TRUE\n");
  162. }
  163. // Date must be GMT dates
  164. // Current date
  165. fwrite($calfileh, "DTSTAMP:".dol_print_date($now, "dayhourxcard", 'gmt')."\n");
  166. // Start date
  167. $prefix = "";
  168. $startdatef = dol_print_date($startdate, "dayhourxcard", 'gmt');
  169. if ($fulldayevent) {
  170. // For fullday event, date was stored with old version by using the user timezone instead of storing the date at UTC+0
  171. // in the timezone of server (so for a PHP timezone of -3, we should store '2023-05-31 21:00:00.000'
  172. // Using option MAIN_STORE_FULL_EVENT_IN_GMT=1 change the behaviour to store in GMT for full day event. This must become
  173. // the default behaviour but there is no way to change keeping old saved date compatible.
  174. $tzforfullday = getDolGlobalString('MAIN_STORE_FULL_EVENT_IN_GMT');
  175. // Local time should be used to prevent users in time zones earlier than GMT from being one day earlier
  176. $prefix = ";VALUE=DATE";
  177. if ($tzforfullday) {
  178. $startdatef = dol_print_date($startdate, "dayxcard", 'gmt');
  179. } else {
  180. $startdatef = dol_print_date($startdate, "dayxcard", 'tzserver');
  181. }
  182. }
  183. fwrite($calfileh, "DTSTART".$prefix.":".$startdatef."\n");
  184. // End date
  185. if ($fulldayevent) {
  186. if (empty($enddate)) {
  187. // We add 1 day needed for full day event (DTEND must be next day after event).
  188. // This is mention in https://datatracker.ietf.org/doc/html/rfc5545:
  189. // "The "DTEND" property for a "VEVENT" calendar component specifies the non-inclusive end of the event."
  190. $enddate = dol_time_plus_duree($startdate, 1, "d");
  191. }
  192. } else {
  193. if (empty($enddate)) {
  194. $enddate = $startdate + $duration;
  195. }
  196. }
  197. $prefix = "";
  198. $enddatef = dol_print_date($enddate, "dayhourxcard", 'gmt');
  199. if ($fulldayevent) {
  200. $prefix = ";VALUE=DATE";
  201. // We add 1 second so we reach the +1 day needed for full day event (DTEND must be next day after event)
  202. // This is mention in https://datatracker.ietf.org/doc/html/rfc5545:
  203. // "The "DTEND" property for a "VEVENT" calendar component specifies the non-inclusive end of the event."
  204. $enddatef = dol_print_date($enddate + 1, "dayxcard", 'tzserver');
  205. }
  206. fwrite($calfileh, "DTEND".$prefix.":".$enddatef."\n");
  207. fwrite($calfileh, "STATUS:CONFIRMED\n");
  208. if (!empty($transparency)) {
  209. fwrite($calfileh, "TRANSP:".$transparency."\n");
  210. }
  211. if (!empty($category)) {
  212. fwrite($calfileh, "CATEGORIES:".$encoding.$category."\n");
  213. }
  214. fwrite($calfileh, "END:VEVENT\n");
  215. }
  216. // Output the vCard/iCal VJOURNAL object
  217. if ($type === "journal") {
  218. $nbevents++;
  219. fwrite($calfileh, "BEGIN:VJOURNAL\n");
  220. fwrite($calfileh, "UID:".$uid."\n");
  221. if (!empty($email)) {
  222. fwrite($calfileh, "ORGANIZER:MAILTO:".$email."\n");
  223. fwrite($calfileh, "CONTACT:MAILTO:".$email."\n");
  224. }
  225. if (!empty($url)) {
  226. fwrite($calfileh, "URL:".$url."\n");
  227. }
  228. if ($created) {
  229. fwrite($calfileh, "CREATED:".dol_print_date($created, "dayhourxcard", 'gmt')."\n");
  230. }
  231. if ($modified) {
  232. fwrite($calfileh, "LAST-MODIFIED:".dol_print_date($modified, "dayhourxcard", 'gmt')."\n");
  233. }
  234. fwrite($calfileh, "SUMMARY:".$encoding.$summary."\n");
  235. fwrite($calfileh, "DESCRIPTION:".$encoding.$description."\n");
  236. fwrite($calfileh, "STATUS:CONFIRMED\n");
  237. fwrite($calfileh, "CATEGORIES:".$category."\n");
  238. fwrite($calfileh, "LOCATION:".$location."\n");
  239. fwrite($calfileh, "TRANSP:OPAQUE\n");
  240. fwrite($calfileh, "CLASS:CONFIDENTIAL\n");
  241. fwrite($calfileh, "DTSTAMP:".dol_print_date($startdatef, "dayhourxcard", 'gmt')."\n");
  242. fwrite($calfileh, "END:VJOURNAL\n");
  243. }
  244. }
  245. // Footer
  246. fwrite($calfileh, "END:VCALENDAR");
  247. fclose($calfileh);
  248. dolChmod($outputfile);
  249. } else {
  250. dol_syslog("xcal.lib.php::build_calfile Failed to open file ".$outputfile." for writing");
  251. return -2;
  252. }
  253. return $nbevents;
  254. }
  255. /**
  256. * Build a file from an array of events.
  257. * All input data must be encoded in $conf->charset_output
  258. *
  259. * @param string $format "rss"
  260. * @param string $title Title of export
  261. * @param string $desc Description of export
  262. * @param array $events_array Array of events ("uid","startdate","summary","url","desc","author","category","image") or Array of WebsitePage
  263. * @param string $outputfile Output file
  264. * @param string $filter (optional) Filter
  265. * @param string $url Url (If empty, forge URL for agenda RSS export)
  266. * @param string $langcode Language code to show in header
  267. * @return int < 0 if KO, Nb of events in file if OK
  268. */
  269. function build_rssfile($format, $title, $desc, $events_array, $outputfile, $filter = '', $url = '', $langcode = '')
  270. {
  271. global $user, $conf, $langs, $mysoc;
  272. global $dolibarr_main_url_root;
  273. dol_syslog("xcal.lib.php::build_rssfile Build rss file ".$outputfile." to format ".$format);
  274. if (empty($outputfile)) {
  275. // -1 = error
  276. return -1;
  277. }
  278. $nbevents = 0;
  279. $fichier = fopen($outputfile, "w");
  280. if ($fichier) {
  281. // Print header
  282. fwrite($fichier, '<?xml version="1.0" encoding="'.$langs->charset_output.'"?>');
  283. fwrite($fichier, "\n");
  284. fwrite($fichier, '<rss version="2.0">');
  285. fwrite($fichier, "\n");
  286. fwrite($fichier, "<channel>\n");
  287. fwrite($fichier, "<title>".$title."</title>\n");
  288. if ($langcode) {
  289. fwrite($fichier, "<language>".$langcode."</language>\n");
  290. }
  291. // Define $urlwithroot
  292. $urlwithouturlroot = preg_replace("/".preg_quote(DOL_URL_ROOT, "/")."$/i", "", trim($dolibarr_main_url_root));
  293. $urlwithroot = $urlwithouturlroot.DOL_URL_ROOT; // This is to use external domain name found into config file
  294. //$urlwithroot=DOL_MAIN_URL_ROOT; // This is to use same domain name than current
  295. // Url
  296. if (empty($url)) {
  297. $url = $urlwithroot."/public/agenda/agendaexport.php?format=rss&exportkey=".urlencode($conf->global->MAIN_AGENDA_XCAL_EXPORTKEY);
  298. }
  299. fwrite($fichier, "<link><![CDATA[".$url."]]></link>\n");
  300. // Image
  301. if (!empty($mysoc->logo_squarred_small)) {
  302. $urlimage = $urlwithroot.'/viewimage.php?cache=1&amp;modulepart=mycompany&amp;file='.urlencode($mysoc->logo_squarred_small);
  303. if ($urlimage) {
  304. fwrite($fichier, "<image><url><![CDATA[".$urlimage."]]></url><title>'.$title.</title></image>\n");
  305. }
  306. }
  307. foreach ($events_array as $key => $event) {
  308. $eventqualified = true;
  309. if ($filter) {
  310. // TODO Add a filter
  311. $eventqualified = false;
  312. }
  313. if ($eventqualified) {
  314. $nbevents++;
  315. if (is_object($event) && get_class($event) == 'WebsitePage') {
  316. // Convert object into an array
  317. $tmpevent = array();
  318. $tmpevent['uid'] = $event->id;
  319. $tmpevent['startdate'] = $event->date_creation;
  320. $tmpevent['summary'] = $event->title;
  321. $tmpevent['url'] = $event->fullpageurl ? $event->fullpageurl : $event->pageurl.'.php';
  322. $tmpevent['author'] = $event->author_alias ? $event->author_alias : 'unknown';
  323. //$tmpevent['category'] = '';
  324. $tmpevent['desc'] = $event->description;
  325. $tmpevent['image'] = $GLOBALS['website']->virtualhost.'/medias/'.$event->image;
  326. $event = $tmpevent;
  327. }
  328. $uid = $event["uid"];
  329. $startdate = $event["startdate"];
  330. $summary = $event["summary"];
  331. $url = $event["url"];
  332. $author = $event["author"];
  333. $category = $event["category"];
  334. if (!empty($event["image"])) {
  335. $image = $event["image"];
  336. }
  337. /* No place inside a RSS
  338. $priority = $event["priority"];
  339. $fulldayevent = $event["fulldayevent"];
  340. $location = $event["location"];
  341. $email = $event["email"];
  342. */
  343. $description = dol_string_nohtmltag(preg_replace("/<br[\s\/]?>/i", "\n", $event["desc"]), 0);
  344. fwrite($fichier, "<item>\n");
  345. fwrite($fichier, "<title><![CDATA[".$summary."]]></title>\n");
  346. fwrite($fichier, "<link><![CDATA[".$url."]]></link>\n");
  347. fwrite($fichier, "<author><![CDATA[".$author."]]></author>\n");
  348. fwrite($fichier, "<category><![CDATA[".$category."]]></category>\n");
  349. fwrite($fichier, "<description><![CDATA[");
  350. if (!empty($image)) {
  351. fwrite($fichier, '<p><img class="center" src="'.$image.'"/></p>');
  352. }
  353. if ($description) {
  354. fwrite($fichier, $description);
  355. }
  356. // else
  357. // fwrite($fichier, "NoDesc");
  358. fwrite($fichier, "]]></description>\n");
  359. fwrite($fichier, "<pubDate>".date("r", $startdate)."</pubDate>\n");
  360. fwrite($fichier, "<guid isPermaLink=\"true\"><![CDATA[".$uid."]]></guid>\n");
  361. fwrite($fichier, "<source><![CDATA[Dolibarr]]></source>\n");
  362. fwrite($fichier, "</item>\n");
  363. }
  364. }
  365. fwrite($fichier, "</channel>");
  366. fwrite($fichier, "\n");
  367. fwrite($fichier, "</rss>");
  368. fclose($fichier);
  369. dolChmod($outputfile);
  370. }
  371. return $nbevents;
  372. }
  373. /**
  374. * Encode for cal export
  375. *
  376. * @param string $format "vcal" or "ical"
  377. * @param string $string String to encode
  378. * @return string String encoded
  379. */
  380. function format_cal($format, $string)
  381. {
  382. $newstring = $string;
  383. if ($format === "vcal") {
  384. $newstring = quotedPrintEncode($newstring);
  385. }
  386. if ($format === "ical") {
  387. // Replace new lines chars by "\n"
  388. $newstring = preg_replace("/\r\n/i", "\\n", $newstring);
  389. $newstring = preg_replace("/\n\r/i", "\\n", $newstring);
  390. $newstring = preg_replace("/\n/i", "\\n", $newstring);
  391. // Must not exceed 75 char. Cut with "\r\n"+Space
  392. $newstring = calEncode($newstring);
  393. }
  394. return $newstring;
  395. }
  396. /**
  397. * Cut string after 75 chars. Add CRLF+Space.
  398. * line must be encoded in UTF-8
  399. *
  400. * @param string $line String to convert
  401. * @return string String converted
  402. */
  403. function calEncode($line)
  404. {
  405. $out = "";
  406. $newpara = "";
  407. // If mb_ functions exists, it"s better to use them
  408. if (function_exists("mb_strlen")) {
  409. $strlength = mb_strlen($line, "UTF-8");
  410. for ($j = 0; $j < $strlength; $j++) {
  411. // Take char at position $j
  412. $char = mb_substr($line, $j, 1, "UTF-8");
  413. if ((mb_strlen($newpara, "UTF-8") + mb_strlen($char, "UTF-8")) >= 75) {
  414. // CRLF + Space for cal
  415. $out .= $newpara."\r\n ";
  416. $newpara = "";
  417. }
  418. $newpara .= $char;
  419. }
  420. $out .= $newpara;
  421. } else {
  422. $strlength = dol_strlen($line);
  423. for ($j = 0; $j < $strlength; $j++) {
  424. // Take char at position $j
  425. $char = substr($line, $j, 1);
  426. if ((dol_strlen($newpara) + dol_strlen($char)) >= 75) {
  427. // CRLF + Space for cal
  428. $out .= $newpara."\r\n ";
  429. $newpara = "";
  430. }
  431. $newpara .= $char;
  432. }
  433. $out .= $newpara;
  434. }
  435. return trim($out);
  436. }
  437. /**
  438. * Encode into vcal format
  439. *
  440. * @param string $str String to convert
  441. * @param int $forcal (optional) 1 = For cal
  442. * @return string String converted
  443. */
  444. function quotedPrintEncode($str, $forcal = 0)
  445. {
  446. $lines = preg_split("/\r\n/", $str);
  447. $out = "";
  448. foreach ($lines as $line) {
  449. $newpara = "";
  450. // Do not use dol_strlen here, we need number of bytes
  451. $strlength = strlen($line);
  452. for ($j = 0; $j < $strlength; $j++) {
  453. $char = substr($line, $j, 1);
  454. $ascii = ord($char);
  455. if ($ascii < 32 || $ascii === 61 || $ascii > 126) {
  456. $char = "=".strtoupper(sprintf("%02X", $ascii));
  457. }
  458. // Do not use dol_strlen here, we need number of bytes
  459. if ((strlen($newpara) + strlen($char)) >= 76) {
  460. // New line with carray-return (CR) and line-feed (LF)
  461. $out .= $newpara."=\r\n";
  462. // extra space for cal
  463. if ($forcal) {
  464. $out .= " ";
  465. }
  466. $newpara = "";
  467. }
  468. $newpara .= $char;
  469. }
  470. $out .= $newpara;
  471. }
  472. return trim($out);
  473. }
  474. /**
  475. * Decode vcal format
  476. *
  477. * @param string $str String to convert
  478. * @return string String converted
  479. */
  480. function quotedPrintDecode($str)
  481. {
  482. return trim(quoted_printable_decode(preg_replace("/=\r?\n/", "", $str)));
  483. }